chrisgoringe / Comfy-Custom-Node-How-To

An unofficial practical guide to getting started developing custom nodes for ComfyUI
GNU General Public License v3.0
143 stars 5 forks source link

Patching nodes from other custom nodes packages? #20

Open Quasimondo opened 1 month ago

Quasimondo commented 1 month ago

I wonder if you already have some experience with this. I was trying to add an extra widget (at runtime) to a node from a different custom nodes package (not from the core library), but even though I was able to find the class through import libs it seems like I was missing some crucial part.

chrisgoringe commented 1 month ago

I haven't done it... but my instinct would be to use the nodeCreated method to inspect each new node, find the ones you want, and then call addWidget.

chrisgoringe commented 1 month ago

Could maybe say more if you have more details :)

Quasimondo commented 1 month ago

Here is one of my tests where I was trying to override a method in the animatediff library and whilst this causes no errors, it also does not get called when the animatediff module is running:

import custom_nodes
from importlib import import_module

ad_loader = import_module('custom_nodes.ComfyUI-AnimateDiff-Evolved.animatediff')
print("PATCH", vars(ad_loader.nodes_gen1.AnimateDiffLoaderGen1))     

def override_get_context_weights(num_frames: int, fuse_method: str):
    print("ADContextManipulator",num_frames,fuse_method)
    return [1.0]*num_frames

animatediff.context.get_context_weights = override_get_context_weights
Quasimondo commented 1 month ago

But yeah it sounds like I should try to rather patch it in the created node as you suggest.

corbin-hayden13 commented 2 days ago

I created a dependency loader to address this exact issue. I figured since ComfyUI already loads all custom nodes as modules, why try to import them twice? This code, when run after all ComfyUI nodes are loaded, should give you the modeules you need based on the dependencies you specified.

You can define dependencies like so:

node_dependencies = {  # Must include path as period '.' delimited
    "ComfyUI_IPAdapter_plus.IPAdapterPlus": ["IPAdapterAdvanced", "IPAdapterUnifiedLoader"],
    "ComfyUI-KJNodes.nodes.mask_nodes": ["CreateFadeMaskAdvanced"],
}

Then call the following code to handle errors of imports not found:

dependencies = load_dependencies(node_dependencies, location="handle_timeline")
    if dependencies is None:
        print("Dependencies returned none, please see console for additional information related to the custom node")
        return None

This is the code for dependency_loader.py:

class Dummy:
    def __init__(self, module_name):
        self.module_name = module_name

def get_dependency(module_name, items):
    module_items = []

    for item in items:
        failed = False
        try:
            module_items.append(getattr(sys.modules[module_name], item))
        except KeyError:
            logging.error(f"TimeUI.dependency_loader KeyError: Couldn't find module {module_name} in sys")
            failed = True
        except AttributeError:
            logging.error(f"TimeUI.dependency_loader AttributeError: {item} not in {module_name}")
            failed = True

        if failed: module_items.append(Dummy(f"{module_name}.{item}"))

    return module_items

def load_dependencies(dependency_dict: dict={}, location: str=None):
    import_success = True
    dependencies = []
    for key, items in dependency_dict.items():
        dependencies.extend(get_dependency(key, items))

    for item in dependencies:
        if type(item) == Dummy:
            import_success = False
            file_path = "sample_file_path/for_verbose_error_messages/"
            logging.error(f"{file_path}{f'at {location}' if location is not None else ''}: could not find required dependency \"{item.module_name}\", please see list of required nodes to install requirements and try again")

    return tuple(dependencies) if import_success else None

This method provides a way of adding to the custom nodes as modules on the python end if that's what you're going for.

corbin-hayden13 commented 2 days ago

But I don't think this answers the original question. If you want to add a widget to an existing node, you need to catch in using the following method:

app.registerExtension({
    async beforeRegisterNodeDef(nodeType, nodeData, app) {
        if (nodeType.comfyClass === "the_comfy_class_of_the_node_you_want_to_hijack") {
            // Your hijack code here;  Recommend adding a customWidget()
            // I would recommend hijacking the `nodeType.prototype.onNodeCreated() {...}` method like so:
            const origOnNodeCreated = nodeType.prototype.onNodeCreated;
            nodeType.prototype.onNodeCreated = function () {
                origOnNodeCreated?.apply(this, arguments);  // Call to make sure the original `onNodeCreated` is called
                // use `this` only under the `onNodeCreated` context as the instanced node you want to add a custom widget to
                // Example:
                const widget = {...};
                this.addCustomWidget(widget);

                // You can also reference any of the widgets added to the node already using this.widgets[index].
                // I'm not sure if overriding a widget like `this.widgets[0] = newWidget;` would work or not, but you could always try!
            }
        }
    }
})

beforeRegisterNodeDef is called for every single node (custom or not) so you can apply all of your overrides here. In fact, all you'd have to do is write an __init__.py file pointing to a js folder like so WEB_DIRECTORY = "./js" and __all__ = ["WEB_DIRECTORY"] then put your javascript file with the app.registerExtension({...}) override in it and you could override any node you want without having to create a node of your own!

Quasimondo commented 2 days ago

Thanks a lot for looking into this and sharing those recipes!