First truly working custom node. A text file loader!
This commit is contained in:
@@ -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.
|
||||
|
||||
+4
-2
@@ -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']
|
||||
+81
-97
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -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)"
|
||||
}
|
||||
Reference in New Issue
Block a user