diff --git a/README.md b/README.md index e69de29..7df0268 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,10 @@ +# Flailing Noises + +Just me failing at both making and understanding how to do this. I'm slowly learning Python, but this playground should help from a practical perspective. + +I finally got the first node working with a lot of back and forth on lack of documentation and help from AI Assistants explaining things to me. + +For example, the first node is simply a text loader. Pick and choose the text file you want to use as an input. For me this is so I don't have to copy/paste large descriptions. Instead I can parse them programatically, use them whole, or even concatonate them with other nodes. All without having to mess with the source material and/or ccidentally wiping out previous prompts, pieces, or tweaks. + +## Future thoughts(?) +I'm hoping to make a personal repository of quality of life type nodes for ComfyUI that make me scratch my head at why the don't exist in the base program. diff --git a/__init__.py b/__init__.py index 1249293..b6ec754 100644 --- a/__init__.py +++ b/__init__.py @@ -1,8 +1,10 @@ -# Import our node classes +# Import your custom node class(es) from their respective files +# This line imports your custom node class (e.g., LoadText) from another Python file within the same custom node directory (e.g., nodes.py). from .nodes import * # This tells ComfyUI to look for a 'js' folder in our directory WEB_DIRECTORY = "js" -# Tell ComfyUI about all the parts of our node +# (Optional) List the names that should be imported when using 'from package import *' +# This list defines the public interface of your module, specifying what names are imported when a user uses from your_module import *. While not strictly necessary for ComfyUI node registration, it's good practice for larger modules. __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY'] \ No newline at end of file diff --git a/js/textUploadWidget.js b/js/textUploadWidget.js index 9867e7d..29b2ac8 100644 --- a/js/textUploadWidget.js +++ b/js/textUploadWidget.js @@ -1,101 +1,85 @@ -import { app } from "/scripts/app.js"; +import { app } from "../../scripts/app.js"; -// This file just needs to register the new widget type -function textUploadWidget(node, inputName, inputData, app) { - const widget = { - type: inputData[0], // "TEXTUPLOAD" - name: inputName, - size: [128, 88], // Widget size - draw(ctx, node, width, y) {}, // No need to draw anything - computeSize(...args) { - return [128, 88]; // Match the size - }, - async serializeValue(nodeId, widgetIndex) { - return node.widgets[widgetIndex].value; - }, - }; - - // Create the "Choose File" button - const fileInput = document.createElement("input"); - fileInput.type = "file"; - fileInput.accept = "text/plain,.txt"; // <--- This is the important change - fileInput.style.display = "none"; - - // Handle file selection - fileInput.addEventListener("change", (event) => { - if (fileInput.files.length) { - uploadFile(fileInput.files[0]); - } - }); - - // Create the button that opens the file picker - const uploadButton = document.createElement("button"); - uploadButton.type = "button"; - uploadButton.innerText = "Choose .txt File to Upload"; - uploadButton.className = "comfy-btn"; - uploadButton.style.width = "100%"; // Make button fill widget - uploadButton.style.marginTop = "10px"; - uploadButton.addEventListener("click", () => { - fileInput.click(); - }); - - // Function to handle the upload - async function uploadFile(file) { - const body = new FormData(); - // We use "image" to match the server's /upload/image endpoint - // The server doesn't care about the file type, just saves it. - body.append("image", file); - body.append("overwrite", "true"); - widget.value = file.name; // Store the filename for the backend - - uploadButton.innerText = "Uploading..."; +// This is the "correct" way to add frontend logic +app.registerExtension({ + name: "Rathius.TextUploader", // Unique name for our extension + + async beforeRegisterNodeDef(nodeType, nodeData, app) { + // This runs for every node *before* it's added - try { - const response = await fetch("/upload/image", { - method: "POST", - body: body, - }); + // Check if this is our node by its Python class name + if (nodeData.name === "LoadTextFile_Rathius") { + + // This function will be called *after* the node is created on the canvas + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + onNodeCreated?.apply(this, arguments); - if (response.status === 200) { - uploadButton.innerText = file.name; // Show the new filename - node.setValue(file.name); // Set the node's internal value - } else { - alert(response.status + " - " + response.statusText); - uploadButton.innerText = "Choose .txt File to Upload"; - } - } catch (error) { - alert(error); - uploadButton.innerText = "Choose .txt File to Upload"; + // Find the default "file_name" widget we created in Python + const widget = this.widgets.find((w) => w.name === "file_name"); + + // Create the "Choose File" button + const fileInput = document.createElement("input"); + fileInput.type = "file"; + fileInput.accept = "text/plain,.txt"; + fileInput.style.display = "none"; + + // Create the button widget + const uploadButton = this.addWidget("button", "Choose .txt File", "None", () => { + fileInput.click(); // Open the file picker when clicked + }); + uploadButton.serialize = false; // Don't save this button's value in workflows + + // Handle what happens when a file is selected + fileInput.addEventListener("change", (event) => { + if (fileInput.files.length) { + uploadFile(fileInput.files[0]); + } + }); + + // Create the upload function + const uploadFile = async (file) => { + const body = new FormData(); + + // 'input' is the default folder, but we can specify a subfolder + body.append("subfolder", "text_prompts"); + body.append("overwrite", "true"); // Overwrite existing files + // The server's /upload/image endpoint handles *any* file + body.append("image", file, file.name); + + uploadButton.name = "Uploading..."; + + try { + // Send the file to the server + const response = await fetch("/upload/image", { + method: "POST", + body: body, + }); + + if (response.status === 200) { + const data = await response.json(); + // Set the value of the *original* (hidden) widget + // This is what Python will receive + const filePath = data.name; // e.g., "text_prompts/my_prompt.txt" + widget.value = filePath; + uploadButton.name = `Loaded: ${filePath}`; + } else { + alert(response.status + " - " + response.statusText); + uploadButton.name = "Choose .txt File"; + } + } catch (error) { + alert(error); + uploadButton.name = "Choose .txt File"; + } + }; + + // Add the hidden file input to the document + document.body.appendChild(fileInput); + + // Hide the original string widget + widget.type = "hidden"; // We don't want to see the text box + widget.serialize = true; // We *do* want to save its value + }; } - } - - // Add the button to the node's widget area - node.addDOMWidget(inputName, "button", uploadButton, widget); - document.body.appendChild(fileInput); // Add the hidden file input - - // Handle drag and drop - node.onDragOver = function (e) { - if (e.dataTransfer.items.length > 0) { - const item = e.dataTransfer.items[0]; - // Allow .txt files (which often have no MIME type) or text/plain - return item.kind === 'file' && (item.type === "text/plain" || item.type === ""); - } - return false; - }; - - node.onDragDrop = function (e) { - let handled = false; - for (const file of e.dataTransfer.files) { - if (file.type === "text/plain" || file.name.endsWith(".txt")) { - uploadFile(file); - handled = true; - } - } - return handled; - }; - - return widget; -} - -// Register our new "TEXTUPLOAD" widget with ComfyUI -app.registerWidget("TEXTUPLOAD", textUploadWidget); \ No newline at end of file + }, +}); \ No newline at end of file diff --git a/nodes.py b/nodes.py index fd4d0db..01fb691 100644 --- a/nodes.py +++ b/nodes.py @@ -1,61 +1,66 @@ +# Required boilerplate for ComfyUI custom nodes. +# - class LoadTextFile: - "Hello, I am a new node." +# - INPUT_TYPES - "Here is the form for the inputs I need." +# - RETURN_TYPES - "Here is what I will give back." +# - FUNCTION - "This is the name of the function that does the actual work." +# - CATEGORY - "Put me in this menu, please." +# - load_text(self, ...) - "I am the actual function that does the work." +# - NODE_CLASS_MAPPINGS - "This is the official registration form." + +# Import any necessary modules import os import folder_paths +import hashlib -# --- The Node Class --- -class LoadTextFile: - """ - A node that loads text from a file. - It uses a custom widget to upload a .txt file. - """ - +# Here you are naming your node class +class LoadTextFile_Rathius: @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { "required": { - # This string "image" MUST match the name in the - # FormData.append("image", file) in the JS. - # It tells ComfyUI to use our new "TEXTUPLOAD" widget. - "image": ("TEXTUPLOAD", ) + # This is just a placeholder text box. + # Our JS will find this widget by its name ("file_name") + # and replace it with an upload button. + "file_name": ("STRING", {"default": "example.txt", "multiline": False}), } } - - # What the node will output + RETURN_TYPES = ("STRING",) RETURN_NAMES = ("text",) - - # The function that runs when the node is executed FUNCTION = "load_text" + CATEGORY = "RathiusNodes/Utils" - # The category in the right-click menu - CATEGORY = "Rathius_ComfyNodes/Utils" - - # We don't want this node to run on file load, only when queued - def IS_CHANGED(self, **kwargs): - return float("inf") - - def load_text(self, image): - # 'image' is the filename (e.g., "my_prompt.txt") - # The widget has already uploaded it to the 'input' folder. - file_path = os.path.join(folder_paths.get_input_directory(), image) - + @classmethod + def IS_CHANGED(cls, file_name): + # This re-runs the node if the file name changes try: - # Open and read the file + file_path = folder_paths.get_annotated_filepath(file_name) + except: + # File doesnt exist yet + return float("inf") + + m = hashlib.sha256() + with open(file_path, 'rb') as f: + m.update(f.read()) + return m.digest().hex() + + def load_text(self, file_name): + # This function is more robust. It searches in all known input folders. + try: + file_path = folder_paths.get_annotated_filepath(file_name) with open(file_path, 'r', encoding='utf-8') as f: text_content = f.read() - - # Return the text content as a tuple return (text_content, ) - except Exception as e: print(f"[LoadTextFile] Error: {e}") - return (f"Error loading file: {e}", ) + # Return the error message as a string so it can be debugged + return (f"Error: {e}", ) # --- Node Registration --- NODE_CLASS_MAPPINGS = { - "LoadTextFile_Rathius": LoadTextFile + "LoadTextFile_Rathius": LoadTextFile_Rathius } -# I updated the display name to reflect its new function NODE_DISPLAY_NAME_MAPPINGS = { "LoadTextFile_Rathius": "Load Text from File (Upload)" } \ No newline at end of file