diff --git a/web/js/nodetype_input.js b/web/js/nodetype_input.js index 532dec2..bab1f9b 100644 --- a/web/js/nodetype_input.js +++ b/web/js/nodetype_input.js @@ -1,47 +1,58 @@ import { chainCallback } from "./utils.js"; -import { ComfyWidgets } from '../../../scripts/widgets.js' -import { addInputs, cleanInputs, clearInputs } from "./inputs.js"; +import { ComfyWidgets } from '../../../scripts/widgets.js'; +import { addInputs, cleanInputs, clearInputs } from "./inputs.js"; // Ensure clearInputs is imported import { colors, bg_colors, node_type_list } from "./constants.js"; -function initialisation(node) { - node.widgets[1].callback = ( value ) => { - cleanInputs(node); +// Graph-independent part of initialization +function initialisation_preGraph(node) { + if (!node.widgets || node.widgets.length < 2) { + console.error("Node widgets not properly initialized for callback setup.", node.type, node.id); + return; + } + node.widgets[1].callback = (value) => { + if (node.graph) { // Still check for graph here for safety, though cleanInputs might be called from onAdded too + clearInputs(node); + } - switch(value){ + switch (value) { case "STRING": node.addOutput("output", "STRING"); ComfyWidgets.STRING( node, "default", - ["STRING",{default: "",},], - app, - ) - + ["STRING", { default: "" }], + app + ); break; case "INT": node.addOutput("output", "INT"); ComfyWidgets.INT( node, "default", - ['INT',{default: 0, "min": 0, "max": 18446744073709551616, "step": 1},], - //['INT',{default: 0},], - app, - ) + ['INT', { default: 0, min: 0, max: 18446744073709551616, step: 1 }], + app + ); break; case "FLOAT": node.addOutput("output", "FLOAT"); ComfyWidgets.FLOAT( node, "default", - ['FLOAT',{default: 0, "min": 0.00, "max": 2048.00, "step": 0.01},], - app, - ) + ['FLOAT', { default: 0, min: 0.00, max: 2048.00, step: 0.01 }], + app + ); break; case "BOOLEAN": node.addOutput("output", "BOOLEAN"); - node.addWidget("toggle", "default", false, ()=>{}); + node.addWidget("toggle", "default", false, () => {}); break; case "none": + // If type is 'none', outputs might have been cleared by clearInputs. + // Ensure no "output" is present if it's truly "none". + if (node.outputs && node.outputs.find(o => o.name === "output")) { + const outputIndex = node.outputs.findIndex(o => o.name === "output"); + if (outputIndex !== -1) node.removeOutput(outputIndex); + } break; default: node.addOutput("output", value); @@ -51,35 +62,65 @@ function initialisation(node) { node.color = colors[node_type_list.indexOf(value)]; node.bgcolor = bg_colors[node_type_list.indexOf(value)]; }; - if (node.widgets[1].value == "none") - clearInputs(node); + node.color = colors[node_type_list.indexOf("none")]; node.bgcolor = bg_colors[node_type_list.indexOf("none")]; } +// Graph-dependent part of initialization +function initialisation_onAdded(node) { + if (!node.widgets || node.widgets.length < 2) { + return; + } + if (node.widgets[1].value === "none") { + clearInputs(node); // This needs node.graph, which is available in onAdded + } +} + function configure(info) { const inputs = {}; - inputs["default"] = {inputs: ["default", info.widgets_values[1], info.widgets_values[2]]}; + inputs["default"] = { inputs: ["default", info.widgets_values[1], info.widgets_values[2]] }; addInputs(this, inputs); } function serialize(info) { - for (let inp of this.inputs){ - if (inp.widget){ - if (inp.type != this.local_input_defs.required[inp.name][0]){ + // Add check for this.local_input_defs + if (!this.inputs || !this.local_input_defs || !this.local_input_defs.required) { + return; // Exit early if the required structures don't exist + } + + for (let inp of this.inputs) { + if (inp.widget) { + // Check that the required path exists before accessing it + if (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]; - const wid = this.widgets.find(w => w.name == inp.name); - if (wid && wid.origType != this.local_input_defs.required[inp.name][0]) + const wid = this.widgets.find(w => w.name === inp.name); + if (wid && wid.origType !== this.local_input_defs.required[inp.name][0]) { wid.origType = this.local_input_defs.required[inp.name][0]; + } } } } } export function setupInputNode(nodeType) { - nodeType.prototype.onNodeCreated = function() { + const originalOnAdded = nodeType.prototype.onAdded; + nodeType.prototype.onAdded = function(graph) { + if (originalOnAdded) { + originalOnAdded.apply(this, arguments); + } + initialisation_onAdded(this); + }; + + nodeType.prototype.onNodeCreated = function () { + this.local_input_defs = this.local_input_defs || { required: {} }; + + initialisation_preGraph(this); + chainCallback(this, "onConfigure", configure); chainCallback(this, "onSerialize", serialize); - initialisation(this); - } + }; } \ No newline at end of file diff --git a/web/js/nodetype_workflow.js b/web/js/nodetype_workflow.js index 9cf014b..3b1fbc9 100644 --- a/web/js/nodetype_workflow.js +++ b/web/js/nodetype_workflow.js @@ -1,76 +1,233 @@ import { chainCallback, fitHeight } from "./utils.js"; -import { addInputs, cleanInputs, removeInputs } from "./inputs.js"; +import { addInputs, cleanInputs, removeInputs, addOutputs } from "./inputs.js"; // Consolidated imports import { importWorkflow } from "./workflows.js"; -import { addOutputs } from "./inputs.js"; import { hideWidget } from "./widgets.js"; -function initialisation(node) { - const workflow_reload = node.title.startsWith("Workflow: ")?true:false; - node.widgets[0].options.values = ["None", ...Object.keys(app.lipsync_studio)] - node.widgets[0].callback = ( value ) => { - cleanInputs(node); - if (value == "None"){ - node.title = "Workflow (FlowChain ⛓️)"; - }else{ - node.widgets[1].value = importWorkflow(node, value, app); - const inputs = app.lipsync_studio[value].inputs; - addInputs(node, inputs, []); - addOutputs(node, value); - fitHeight(node); - } - }; - if (!workflow_reload){ - node.widgets[0].value = "None"; - node.widgets[1].value = ""; - } - hideWidget(node, node.widgets[1], { holdSpace: false }) - cleanInputs(node); +// Graph-independent part of initialization (called onNodeCreated) +function initialisation_preGraph(node) { 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"]; + } + } + + 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 ⛓️)"; + } + }; + } +} + +// 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; + } + + // 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 + } + } else { + node.title = "Workflow (FlowChain ⛓️)"; + } + } } function configure(info) { - if (info.widgets_values[0] != "None"){ - const inputs = app.lipsync_studio[info.widgets_values[0]].inputs; + // `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; + } - 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) - .then(data => { - if (data){ - this.widgets[1].value = data; - const inputs = app.lipsync_studio[info.widgets_values[0]].inputs; + // 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)]; + } - addInputs(this, inputs, info.widgets_values); - addOutputs(this, info.widgets_values[0]); - removeInputs(this, inputs, info.widgets_values); - fitHeight(this); - } - }) - .catch(error => { - console.error('Erreur lors de l\'importation:', error); - }); + const 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 (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 (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}`; } } function serialize(info) { - for (let inp of this.inputs){ - if (inp.widget){ - if (inp.type != this.local_input_defs.required[inp.name][0]) + // 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(this.widgets[0].options.values == "COMBO"){ - this.widgets[0].options.values = ["None", ...Object.keys(app.lipsync_studio)]; - } } export function setupWorkflowNode(nodeType) { - nodeType.prototype.onNodeCreated = function() { + 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); + } + }; + + 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 + chainCallback(this, "onConfigure", configure); chainCallback(this, "onSerialize", serialize); - initialisation(this); - } + }; } \ No newline at end of file diff --git a/workflow.py b/workflow.py index 170d10b..61bf680 100644 --- a/workflow.py +++ b/workflow.py @@ -14,6 +14,27 @@ import gc import folder_paths from server import PromptServer from execution import PromptExecutor +from comfy.model_patcher import ModelPatcher # Added import +from comfy.sd import CLIP, VAE # Added imports + + +# Add this custom JSON Encoder class +class ComfyUIObjectEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, torch.Tensor): + return f"" + elif isinstance(obj, ModelPatcher): + # Represent ModelPatcher as a descriptive string + # You could add more details if needed, e.g., obj.model.__class__.__name__ + return f"" + elif isinstance(obj, CLIP): + # Represent CLIP as a descriptive string + return f"" + elif isinstance(obj, VAE): + # Represent VAE as a descriptive string + return f"" + # Let the base class default method raise the TypeError for other types + return json.JSONEncoder.default(self, obj) class ExecutionResult(Enum): @@ -428,6 +449,13 @@ class Workflow(SaveImage): simple_server = SimpleServer() executor = PromptExecutor(simple_server) + # Before executing, you can print the workflow structure for debugging + print(json.dumps(workflow, indent=2, cls=ComfyUIObjectEncoder)) # MODIFIED: Use the new encoder + # Or specifically find the ShowText node if you know its ID or can iterate + for node_id, node_data in workflow.items(): + if node_data.get("class_type") == "ShowText": # Or whatever the exact class_type is + print(f"ShowText Node {node_id} inputs: {node_data.get('inputs')}") + executor.execute(workflow, prompt_id, {"client_id": client_id}, workflow_outputs_id) history_result = executor.history_result