├── README.md ├── __init__.py ├── imgs ├── adv_datetime_use.png ├── basic_useage.png ├── multi_variable_formatting.png └── variable_formatting.png ├── nodes.py └── web └── js ├── showPath.js └── showString.js /README.md: -------------------------------------------------------------------------------- 1 | # ComfyUI Path Helper 2 | 3 | This module provides a set of custom nodes to help with creating file paths for ComfyUI. Paths are joined using python os library. This should help issues where people are manually constructing paths with strings which then dont work for someone if they are using a different OS then them 4 | 5 | ## Features 6 | 7 | - **Create project root folder** 8 | - **Add Folders** 9 | - **Add file prefix** 10 | - **Show path/strings in a node** 11 | - **Prefix/Postfix datetime to paths** 12 | - **Format variables into paths** 13 | 14 | ## Nodes 15 | #### CreateProjectRoot 16 | #### AddFolder 17 | #### AddFolderAdvanced 18 | #### AddFileNamePrefix 19 | #### AddFileNamePrefixAdvanced 20 | #### JoinVariables 21 | #### ShowPath 22 | #### ShowString 23 | 24 | ## Basic usage 25 | ![basic_useage.png](imgs%2Fbasic_useage.png) 26 | ## Advanced usage 27 | 28 | ### Date Time 29 | Advanced Nodes allow you to prefix or postfix datetime to a path 30 | Datetime is formatted using strftime so can be customized to any format you like 31 | more info on strftime here https://strftime.org/ 32 | ![adv_datetime_use.png](imgs%2Fadv_datetime_use.png) 33 | 34 | ### Variable formatting 35 | Advanced Nodes also allow for the formatting of variables with the node being constructed. Use the {} notion where you wish for variable to be formatted within the given string 36 | ![variable_formatting.png](imgs%2Fvariable_formatting.png) 37 | 38 | ### Multiple Variable formatting 39 | If you wish to format multiple variables then this is also possible by passing a comma seperated string into 'input_variables'. You can use the Join Variables node to do this for you. 40 | Note order of variables is import 41 | ![multi_variable_formatting.png](imgs%2Fmulti_variable_formatting.png) 42 | 43 | ** note input_variables will accept any type and attempt to format. I wanted to be able to pass strings, ints, floats etc but given its unrestricted this may break if given types such a LATENT 44 | 45 | ** primate node doesnt work however 46 | 47 | ## Known issues 48 | Show Path seems to always hold first value it receives and then will redner a second text window with any updated values. Not sure why this is happening. Appears to be something related to PATH being a custom type -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from .nodes import * 2 | 3 | NODE_CLASS_MAPPINGS = { 4 | "Create Project Root": CreateProjectRoot, 5 | "Add Folder": AddFolder, 6 | "Add Folder Advanced": AddFolderAdvanced, 7 | "Add File Name Prefix": AddFileNamePrefix, 8 | "Add File Name Prefix Advanced": AddFileNamePrefixAdvanced, 9 | "Join Variables": JoinVariables, 10 | "Show Path": ShowPath, 11 | "Show String": ShowString, 12 | } 13 | 14 | WEB_DIRECTORY = "./web" 15 | __all__ = ["NODE_CLASS_MAPPINGS", "WEB_DIRECTORY"] 16 | print("\033[34mComfyUI Path Helper Nodes: \033[92mLoaded\033[0m") 17 | -------------------------------------------------------------------------------- /imgs/adv_datetime_use.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billius-AI/ComfyUI-Path-Helper/6b190bcd1a6fc17483c3e978eaeaecb47837d95e/imgs/adv_datetime_use.png -------------------------------------------------------------------------------- /imgs/basic_useage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billius-AI/ComfyUI-Path-Helper/6b190bcd1a6fc17483c3e978eaeaecb47837d95e/imgs/basic_useage.png -------------------------------------------------------------------------------- /imgs/multi_variable_formatting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billius-AI/ComfyUI-Path-Helper/6b190bcd1a6fc17483c3e978eaeaecb47837d95e/imgs/multi_variable_formatting.png -------------------------------------------------------------------------------- /imgs/variable_formatting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billius-AI/ComfyUI-Path-Helper/6b190bcd1a6fc17483c3e978eaeaecb47837d95e/imgs/variable_formatting.png -------------------------------------------------------------------------------- /nodes.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | import folder_paths 4 | 5 | 6 | class AnyType(str): 7 | def __ne__(self, __value: object) -> bool: 8 | return False 9 | 10 | 11 | ANY = AnyType("*") 12 | 13 | 14 | def format_date_time(string, position, datetime_format): 15 | today = datetime.now() 16 | if position == "prefix": 17 | return f"{today.strftime(datetime_format)}_{string}" 18 | if position == "postfix": 19 | return f"{string}_{today.strftime(datetime_format)}" 20 | 21 | 22 | def format_variables(string, input_variables): 23 | if input_variables: 24 | variables = str(input_variables).split(",") 25 | return string.format(*variables) 26 | else: 27 | return string 28 | 29 | 30 | # 31 | class CreateProjectRoot: 32 | def __init__(self): 33 | self.output_dir = folder_paths.get_output_directory() 34 | 35 | @classmethod 36 | def INPUT_TYPES(cls): 37 | return { 38 | "required": { 39 | "project_root_name": ("STRING", {"multiline": False, "default": ""}), 40 | "output_path_generation": (["relative", "absolute"],) 41 | } 42 | } 43 | 44 | RETURN_TYPES = ("PATH",) 45 | FUNCTION = "create_project_root" 46 | CATEGORY = "path_helper" 47 | 48 | def create_project_root(self, project_root_name, output_path_generation): 49 | if output_path_generation == "relative": 50 | return ("./" + project_root_name,) 51 | elif output_path_generation == "absolute": 52 | return (os.path.join(self.output_dir, project_root_name),) 53 | 54 | 55 | class AddFolder: 56 | @classmethod 57 | def INPUT_TYPES(cls): 58 | return { 59 | "required": { 60 | "path": ("PATH",), 61 | "folder_name": ("STRING", {"multiline": False, "default": ""}), 62 | } 63 | } 64 | 65 | RETURN_TYPES = ("PATH",) 66 | FUNCTION = "add_folder" 67 | CATEGORY = "path_helper" 68 | 69 | def add_folder(self, path, folder_name): 70 | new_path = os.path.join(path, folder_name) 71 | return (new_path,) 72 | 73 | 74 | class AddFolderAdvanced: 75 | @classmethod 76 | def INPUT_TYPES(cls): 77 | return { 78 | "required": { 79 | "path": ("PATH",), 80 | "folder_name": ("STRING", {"multiline": False, "default": ""}), 81 | "add_date_time": (["disable", "prefix", "postfix"],), 82 | "date_time_format": ("STRING", {"multiline": False, "default": "%Y_%m_%d_%H:%M:%S"}), 83 | }, 84 | "optional": { 85 | "input_variables": (ANY,) 86 | } 87 | } 88 | 89 | RETURN_TYPES = ("PATH",) 90 | FUNCTION = "add_folder" 91 | CATEGORY = "path_helper" 92 | 93 | def add_folder(self, path, folder_name, add_date_time, date_time_format, input_variables=None): 94 | folder_name_parsed = format_variables(folder_name, input_variables) 95 | if add_date_time == "disable": 96 | new_path = os.path.join(path, folder_name_parsed) 97 | else: 98 | new_path = os.path.join(path, format_date_time(folder_name_parsed, add_date_time, date_time_format)) 99 | return (new_path,) 100 | 101 | 102 | class AddFileNamePrefix: 103 | @classmethod 104 | def INPUT_TYPES(cls): 105 | return { 106 | "required": { 107 | "path": ("PATH",), 108 | "file_name_prefix": ("STRING", {"multiline": False, "default": ""}), 109 | } 110 | } 111 | 112 | RETURN_TYPES = ("STRING",) 113 | FUNCTION = "add_filename_prefix" 114 | CATEGORY = "path_helper" 115 | 116 | def add_filename_prefix(self, path, file_name_prefix): 117 | new_path = os.path.join(path, file_name_prefix) 118 | return (new_path,) 119 | 120 | 121 | class AddFileNamePrefixAdvanced: 122 | @classmethod 123 | def INPUT_TYPES(cls): 124 | return { 125 | "required": { 126 | "path": ("PATH",), 127 | "file_name_prefix": ("STRING", {"multiline": False, "default": ""}), 128 | "add_date_time": (["disable", "prefix", "postfix"],), 129 | "date_time_format": ("STRING", {"multiline": False, "default": "%Y_%m_%d_%H:%M:%S"}), 130 | }, 131 | "optional": { 132 | "input_variables": (ANY,) 133 | } 134 | } 135 | 136 | RETURN_TYPES = ("STRING",) 137 | FUNCTION = "add_filename_prefix" 138 | CATEGORY = "path_helper" 139 | 140 | def add_filename_prefix(self, path, file_name_prefix, add_date_time, date_time_format, input_variables=None): 141 | filename_name_parsed = format_variables(file_name_prefix, input_variables) 142 | if add_date_time == "disable": 143 | new_path = os.path.join(path, filename_name_parsed) 144 | else: 145 | new_path = os.path.join(path, format_date_time(filename_name_parsed, add_date_time, date_time_format)) 146 | return (new_path,) 147 | 148 | 149 | class JoinVariables: 150 | @classmethod 151 | def INPUT_TYPES(cls): 152 | return { 153 | "required": { 154 | "var_1": (ANY,), 155 | }, 156 | "optional": { 157 | "var_2": (ANY,), 158 | "var_3": (ANY,), 159 | "var_4": (ANY,), 160 | } 161 | } 162 | 163 | FUNCTION = "join_variables" 164 | CATEGORY = "path_helper" 165 | RETURN_TYPES = ("STRING",) 166 | 167 | def join_variables(self, var_1, var_2=None, var_3=None, var_4=None): 168 | variables = [var_1, var_2, var_3, var_4] 169 | return (','.join([str(var) for var in variables if var is not None]),) 170 | 171 | 172 | class ShowPath: 173 | @classmethod 174 | def INPUT_TYPES(cls): 175 | return { 176 | "required": { 177 | "path": ("PATH",), 178 | }, 179 | } 180 | 181 | INPUT_IS_LIST = True 182 | FUNCTION = "show_path" 183 | CATEGORY = "path_helper" 184 | OUTPUT_NODE = True 185 | RETURN_TYPES = ("STRING",) 186 | 187 | def show_path(self, path): 188 | return {"ui": {"text": path}, "result": (path,)} 189 | 190 | 191 | class ShowString: 192 | @classmethod 193 | def INPUT_TYPES(cls): 194 | return { 195 | "required": { 196 | "string": ("STRING", {"forceInput": True}), 197 | }, 198 | } 199 | 200 | INPUT_IS_LIST = True 201 | FUNCTION = "show_string" 202 | CATEGORY = "path_helper" 203 | OUTPUT_NODE = True 204 | RETURN_TYPES = ("STRING",) 205 | 206 | def show_string(self, string): 207 | return {"ui": {"text": string}, "result": (string,)} 208 | -------------------------------------------------------------------------------- /web/js/showPath.js: -------------------------------------------------------------------------------- 1 | import {app} from "../../../scripts/app.js"; 2 | import {ComfyWidgets} from "../../../scripts/widgets.js"; 3 | 4 | // Displays input path text on a node 5 | app.registerExtension({ 6 | name: "path_helper.ShowPath", 7 | async beforeRegisterNodeDef(nodeType, nodeData, app) { 8 | if (nodeType.comfyClass === "Show Path") { 9 | function populate(text) { 10 | console.log("sp") 11 | console.log(this.widgets) 12 | if (this.widgets) { 13 | for (let i = 1; i < this.widgets.length; i++) { 14 | this.widgets[i].onRemove?.(); 15 | } 16 | this.widgets.length = 1; 17 | } 18 | 19 | const v = [...text]; 20 | if (!v[0]) { 21 | v.shift(); 22 | } 23 | for (const list of v) { 24 | const w = ComfyWidgets["STRING"](this, "text", ["STRING", {multiline: true}], app).widget; 25 | w.inputEl.readOnly = true; 26 | w.inputEl.style.opacity = 0.6; 27 | w.value = list; 28 | } 29 | 30 | requestAnimationFrame(() => { 31 | const sz = this.computeSize(); 32 | if (sz[0] < this.size[0]) { 33 | sz[0] = this.size[0]; 34 | } 35 | if (sz[1] < this.size[1]) { 36 | sz[1] = this.size[1]; 37 | } 38 | this.onResize?.(sz); 39 | app.graph.setDirtyCanvas(true, false); 40 | }); 41 | } 42 | 43 | // When the node is executed we will be sent the input text, display this in the widget 44 | const onExecuted = nodeType.prototype.onExecuted; 45 | nodeType.prototype.onExecuted = function (message) { 46 | console.log("one") 47 | onExecuted?.apply(this, arguments); 48 | populate.call(this, message.text); 49 | }; 50 | 51 | const onConfigure = nodeType.prototype.onConfigure; 52 | nodeType.prototype.onConfigure = function () { 53 | onConfigure?.apply(this, arguments); 54 | if (this.widgets_values?.length) { 55 | console.log("eeee") 56 | populate.call(this, this.widgets_values); 57 | } 58 | }; 59 | } 60 | }, 61 | }); 62 | 63 | -------------------------------------------------------------------------------- /web/js/showString.js: -------------------------------------------------------------------------------- 1 | import {app} from "../../../scripts/app.js"; 2 | import {ComfyWidgets} from "../../../scripts/widgets.js"; 3 | 4 | app.registerExtension({ 5 | name: "path_helper.ShowString", 6 | async beforeRegisterNodeDef(nodeType, nodeData, app) { 7 | if (nodeType.comfyClass === "Show String") { 8 | function populate(text) { 9 | console.log("ss") 10 | console.log(this.widgets) 11 | if (this.widgets) { 12 | for (let i = 1; i < this.widgets.length; i++) { 13 | this.widgets[i].onRemove?.(); 14 | } 15 | this.widgets.length = 1; 16 | } 17 | 18 | const v = [...text]; 19 | if (!v[0]) { 20 | v.shift(); 21 | } 22 | for (const list of v) { 23 | const w = ComfyWidgets["STRING"](this, "text", ["STRING", {multiline: true}], app).widget; 24 | w.inputEl.readOnly = true; 25 | w.inputEl.style.opacity = 0.6; 26 | w.value = list; 27 | } 28 | 29 | requestAnimationFrame(() => { 30 | const sz = this.computeSize(); 31 | if (sz[0] < this.size[0]) { 32 | sz[0] = this.size[0]; 33 | } 34 | if (sz[1] < this.size[1]) { 35 | sz[1] = this.size[1]; 36 | } 37 | this.onResize?.(sz); 38 | app.graph.setDirtyCanvas(true, false); 39 | }); 40 | } 41 | 42 | // When the node is executed we will be sent the input text, display this in the widget 43 | const onExecuted = nodeType.prototype.onExecuted; 44 | nodeType.prototype.onExecuted = function (message) { 45 | onExecuted?.apply(this, arguments); 46 | populate.call(this, message.text); 47 | }; 48 | 49 | const onConfigure = nodeType.prototype.onConfigure; 50 | nodeType.prototype.onConfigure = function () { 51 | onConfigure?.apply(this, arguments); 52 | if (this.widgets_values?.length) { 53 | populate.call(this, this.widgets_values); 54 | } 55 | }; 56 | } 57 | }, 58 | }); 59 | --------------------------------------------------------------------------------