├── .gitignore ├── README.md ├── __init__.py ├── startup_utils.py ├── operations.py ├── js └── operation-node.js └── nodes.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | __pycache__/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ComfyLiterals 2 | ![CleanShot 2023-07-22 at 00 13 13](https://github.com/M1kep/ComfyLiterals/assets/2661819/c8bdc4f0-8cf3-4403-be96-34db357520b0) 3 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from .nodes import IntLiteral, FloatLiteral, StringLiteral, CheckpointListLiteral, LoraListLiteral 2 | from .operations import Operation 3 | from .startup_utils import symlink_web_dir 4 | 5 | NODE_CLASS_MAPPINGS = { 6 | "Int": IntLiteral, 7 | "Float": FloatLiteral, 8 | "String": StringLiteral, 9 | "KepStringLiteral": StringLiteral, 10 | "Operation": Operation, 11 | "Checkpoint": CheckpointListLiteral, 12 | "Lora": LoraListLiteral, 13 | } 14 | 15 | NODE_DISPLAY_NAME_MAPPINGS = { 16 | "KepStringLiteral": "String", 17 | } 18 | 19 | EXTENSION_NAME = "ComfyLiterals" 20 | 21 | symlink_web_dir("js", EXTENSION_NAME) 22 | -------------------------------------------------------------------------------- /startup_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | import folder_paths 5 | 6 | 7 | def symlink_web_dir(local_path, extension_name): 8 | comfy_web_ext_root = Path(os.path.join(folder_paths.base_path, "web", "extensions")) 9 | target_dir = Path(os.path.join(comfy_web_ext_root, extension_name)) 10 | extension_path = Path(__file__).parent.resolve() 11 | 12 | if target_dir.exists(): 13 | print(f"Web extensions folder found at {target_dir}") 14 | elif comfy_web_ext_root.exists(): 15 | try: 16 | os.symlink((os.path.join(extension_path, local_path)), target_dir) 17 | except OSError as e: # OSError 18 | print( 19 | f"Error:\n{e}\n" 20 | f"Failed to create symlink to {target_dir}. Please copy the folder manually.\n" 21 | f"Source: {os.path.join(extension_path, local_path)}\n" 22 | f"Target: {target_dir}" 23 | ) 24 | except Exception as e: 25 | print(f"Unexpected error:\n{e}") 26 | else: 27 | print( 28 | f"Failed to find comfy root automatically, please copy the folder {os.path.join(extension_path, 'web')} manually in the web/extensions folder of ComfyUI" 29 | ) 30 | -------------------------------------------------------------------------------- /operations.py: -------------------------------------------------------------------------------- 1 | class Operation: 2 | def __init__(self, ): 3 | pass 4 | 5 | @classmethod 6 | def INPUT_TYPES(s): 7 | return { 8 | "required": { 9 | "A Type": (["Int", "Float"],), 10 | "B Type": (["Int", "Float"],), 11 | "Operation": (["A+B", "A-B", "A*B", "A/B"],) 12 | }, 13 | "optional": { 14 | "A - Int": ("INT", {"forceInput": True}), 15 | "A - Float": ("FLOAT", {"forceInput": True}), 16 | "B - Int": ("INT", {"forceInput": True}), 17 | "B - Float": ("FLOAT", {"forceInput": True}) 18 | } 19 | } 20 | 21 | RETURN_TYPES = ("INT", "FLOAT") 22 | FUNCTION = "do_operation" 23 | 24 | CATEGORY = "Literals" 25 | 26 | def _do_addition(self, a_val, b_val): 27 | return (int(a_val + b_val), float(a_val + b_val)) 28 | 29 | def _do_subtraction(self, a_val, b_val): 30 | return (int(a_val - b_val), float(a_val - b_val)) 31 | 32 | def _do_multiplication(self, a_val, b_val): 33 | return (int(a_val * b_val), float(a_val * b_val)) 34 | 35 | def _do_division(self, a_val, b_val): 36 | return (int(a_val / b_val), float(a_val / b_val)) 37 | 38 | def do_operation(self, **kwargs): 39 | print(f"PrintNode: {kwargs}") 40 | is_a_int = kwargs["A Type"] == "Int" 41 | is_b_int = kwargs["B Type"] == "Int" 42 | a_val = kwargs["A - Int"] if is_a_int else kwargs["A - Float"] 43 | b_val = kwargs["B - Int"] if is_b_int else kwargs["B - Float"] 44 | 45 | if kwargs["Operation"] == "A+B": 46 | return self._do_addition(a_val, b_val) 47 | elif kwargs["Operation"] == "A-B": 48 | return self._do_subtraction(a_val, b_val) 49 | elif kwargs["Operation"] == "A*B": 50 | return self._do_multiplication(a_val, b_val) 51 | elif kwargs["Operation"] == "A/B": 52 | return self._do_division(a_val, b_val) 53 | else: 54 | raise Exception("Invalid operation provided") 55 | -------------------------------------------------------------------------------- /js/operation-node.js: -------------------------------------------------------------------------------- 1 | import {app} from "/scripts/app.js"; 2 | 3 | app.registerExtension({ 4 | name: "ComfyLiterals.OperationNode", 5 | nodeCreated(node, app) { 6 | if (node['comfyClass'] === 'Operation') { 7 | const onAdded = node.onAdded 8 | node.onAdded = function (graph) { 9 | console.log("OperationNode onAdded") 10 | const firstCallbackResp = onAdded ? onAdded.apply(this, arguments) : undefined; 11 | 12 | /** 13 | * @type {Record} 14 | */ 15 | const inputCache = { 16 | "A": node.inputs[1], 17 | "B": node.inputs[3] 18 | } 19 | 20 | if (this.widgets_values) { 21 | const aType = this.widgets_values[0] 22 | const bType = this.widgets_values[1] 23 | 24 | // [IntA, FloatA, IntB, FloatB] 25 | const aIdxToDelete = aType === "INT" ? 1 : 0 26 | // [*A, IntB, FloatB] 27 | const bIdxToDelete = bType === "INT" ? 3 : 1 28 | 29 | inputCache["A"] = node.inputs[aIdxToDelete] 30 | this.removeInput(aIdxToDelete) 31 | inputCache["B"] = node.inputs[bIdxToDelete] 32 | this.removeInput(bIdxToDelete) 33 | } else { 34 | // Nodes being restored/pasted don't have widget_values 35 | // Node has 4 inputs(IntA, FloatA, IntB, FloatB) 36 | // Remove both float inputs, Float B moves to index 2 after Float A is removed 37 | this.removeInput(1) 38 | this.removeInput(2) 39 | } 40 | 41 | // Add a toggle widget to the node 42 | this.widgets[0].callback = function (v, canvas, node) { 43 | addInputAtIndex(node, inputCache["A"], 0) 44 | inputCache["A"] = node.inputs[1] 45 | node.removeInput(1) 46 | } 47 | this.widgets[1].callback = function (v, canvas, node) { 48 | addInputAtIndex(node, inputCache["B"], 2) 49 | inputCache["B"] = node.inputs[1] 50 | node.removeInput(1) 51 | } 52 | } 53 | } 54 | } 55 | }) 56 | 57 | /** 58 | * Adds an input to a node at the given index. 59 | * @param node {LGraphNode} 60 | * @param input {INodeInputSlot} 61 | * @param index {number} 62 | * @returns {INodeInputSlot} 63 | */ 64 | function addInputAtIndex(node, input, index) { 65 | if (!node.inputs) { 66 | node.inputs = []; 67 | } 68 | 69 | if (index > node.inputs.length) { 70 | console.warn("LiteGraph: Warning adding port index: " + index + " of node " + node.id + ", it doesnt have so many inputs"); 71 | node.inputs.push(input); 72 | } else { 73 | node.inputs.splice(index, 0, input); 74 | } 75 | if (node.onInputAdded) { 76 | node.onInputAdded(input); 77 | } 78 | node.setSize(node.computeSize()); 79 | LiteGraph.registerNodeAndSlotType(node, input.type || 0); 80 | 81 | node.setDirtyCanvas(true, true); 82 | return input; 83 | } 84 | -------------------------------------------------------------------------------- /nodes.py: -------------------------------------------------------------------------------- 1 | import folder_paths 2 | 3 | # Hack: string type that is always equal in not equal comparisons 4 | class AnyType(str): 5 | def __ne__(self, __value: object) -> bool: 6 | return False 7 | 8 | 9 | # Our any instance wants to be a wildcard string 10 | ANY = AnyType("*") 11 | class IntLiteral: 12 | def __init__(self, ): 13 | pass 14 | 15 | @classmethod 16 | def INPUT_TYPES(s): 17 | return { 18 | "required": { 19 | "Number": ("STRING", {}), 20 | } 21 | } 22 | 23 | RETURN_TYPES = ("INT",) 24 | FUNCTION = "to_int" 25 | 26 | CATEGORY = "Literals" 27 | 28 | def to_int(self, Number): 29 | try: 30 | ret_val = int(Number) 31 | except Exception: 32 | raise Exception("Invalid value provided for INT") 33 | return (ret_val,) 34 | 35 | 36 | class FloatLiteral: 37 | def __init__(self, ): 38 | pass 39 | 40 | @classmethod 41 | def INPUT_TYPES(s): 42 | return { 43 | "required": { 44 | "Number": ("STRING", {}), 45 | } 46 | } 47 | 48 | RETURN_TYPES = ("FLOAT",) 49 | FUNCTION = "to_float" 50 | 51 | CATEGORY = "Literals" 52 | 53 | def to_float(self, Number): 54 | try: 55 | ret_val = float(Number) 56 | except Exception: 57 | raise Exception("Invalid value provided for FLOAT") 58 | return (ret_val,) 59 | 60 | 61 | class StringLiteral: 62 | def __init__(self, ): 63 | pass 64 | 65 | @classmethod 66 | def INPUT_TYPES(s): 67 | return { 68 | "required": { 69 | "String": ("STRING", {"multiline": True}), 70 | } 71 | } 72 | 73 | RETURN_TYPES = ("STRING",) 74 | FUNCTION = "to_string" 75 | 76 | CATEGORY = "Literals" 77 | 78 | def to_string(self, String): 79 | return (String,) 80 | 81 | 82 | class CheckpointListLiteral: 83 | def __init__(self): 84 | pass 85 | 86 | @classmethod 87 | def INPUT_TYPES(s): 88 | return { 89 | "required": { 90 | "literal": ("STRING", { 91 | "multiline": True, 92 | "default": "\n".join(folder_paths.get_filename_list("checkpoints")) 93 | }), 94 | }, 95 | } 96 | 97 | RETURN_TYPES = (ANY,) 98 | RETURN_NAMES = ("Selected Checkpoints",) 99 | OUTPUT_IS_LIST = (True,) 100 | FUNCTION = "parse_literal" 101 | 102 | # OUTPUT_NODE = False 103 | 104 | CATEGORY = "List Stuff" 105 | 106 | def parse_literal(self, literal): 107 | split = list(filter(None, literal.split("\n"))) 108 | return (split,) 109 | 110 | class LoraListLiteral: 111 | def __init__(self): 112 | pass 113 | 114 | @classmethod 115 | def INPUT_TYPES(s): 116 | return { 117 | "required": { 118 | "literal": ("STRING", { 119 | "multiline": True, 120 | "default": "\n".join(folder_paths.get_filename_list("loras")) 121 | }), 122 | }, 123 | } 124 | 125 | RETURN_TYPES = (ANY,) 126 | RETURN_NAMES = ("Selected Loras",) 127 | OUTPUT_IS_LIST = (True,) 128 | FUNCTION = "parse_literal" 129 | 130 | # OUTPUT_NODE = False 131 | 132 | CATEGORY = "List Stuff" 133 | 134 | def parse_literal(self, literal): 135 | split = list(filter(None, literal.split("\n"))) 136 | return (split,) 137 | --------------------------------------------------------------------------------