diff --git a/IDENode/README.md b/IDENode/README.md index 277384b..c69a565 100644 --- a/IDENode/README.md +++ b/IDENode/README.md @@ -4,6 +4,10 @@ **IDE Node** is an experimental node that allows you to run code written in **Python** or **Javascript** directly in the node, it is also possible to receive input data, which is stored in variables, process and output the result. +# Changelog: + +- 2024.08.28: Implemented add output variables and types + # Features: - Selecting a syntax highlighting theme diff --git a/IDENode/ide_node.py b/IDENode/ide_node.py index 1ce80a6..21151c9 100644 --- a/IDENode/ide_node.py +++ b/IDENode/ide_node.py @@ -3,7 +3,9 @@ from server import PromptServer from aiohttp import web from asyncio import sleep, run import json +import re +remove_type_name = re.compile(r"(\{.*\})", re.I | re.M) # Hack: string type that is always equal in not equal comparisons, thanks pythongosssss class AnyType(str): @@ -49,6 +51,24 @@ async def wait_js_complete(unique_id, time_out=40): return False +# - Thank you very much for the class -> Trung0246 - +# - https://github.com/Trung0246/ComfyUI-0246/blob/main/utils.py#L51 +class TautologyStr(str): + def __ne__(self, other): + return False + + +class ByPassTypeTuple(tuple): + def __getitem__(self, index): + if index > 0: + index = 0 + item = super().__getitem__(index) + if isinstance(item, str): + return TautologyStr(item) + return item +# --------------------------- + + class IDENode: def __init__(self): self.js_complete = False @@ -78,43 +98,57 @@ result = runCode()""" }, ), }, - "hidden": {"unique_id": "UNIQUE_ID"}, + "hidden": {"unique_id": "UNIQUE_ID", "extra_pnginfo": "EXTRA_PNGINFO"}, } - RETURN_TYPES = (PY_CODE,) - RETURN_NAMES = ("any",) + RETURN_TYPES = ByPassTypeTuple((PY_CODE,)) + RETURN_NAMES = ("result{ANY}",) FUNCTION = "exec_py" CATEGORY = "AlekPet Nodes/experiments" - def exec_py(self, pycode, language, unique_id, **kwargs): + def exec_py(self, pycode, language, unique_id, extra_pnginfo, **kwargs): if unique_id not in IDEs_DICT: IDEs_DICT[unique_id] = self - if language == "python": - my_namespace = types.SimpleNamespace() - my_namespace.__dict__.update(kwargs) - my_namespace.__dict__.setdefault("result", "The result variable is not assigned") + outputs = {} + + for node in extra_pnginfo['workflow']['nodes']: + if node['id'] == int(unique_id): + outputs_valid = [ouput for ouput in node.get('outputs', []) if ouput.get('name','') != '' and ouput.get('type','') != ''] + outputs = {re.sub(remove_type_name, "", ouput['name']): None for ouput in outputs_valid} + self.RETURN_TYPES = ByPassTypeTuple(out["type"] for out in outputs_valid) + self.RETURN_NAMES = tuple(name for name in outputs.keys()) + + my_namespace = types.SimpleNamespace() + my_namespace.__dict__.update(outputs) + my_namespace.__dict__.update({re.sub(remove_type_name, "", prop): kwargs[prop] for prop in kwargs}) + my_namespace.__dict__.setdefault("result", "The result variable is not assigned") + + if language == "python": try: exec(pycode, my_namespace.__dict__) except Exception as e: my_namespace.result = f"Error in python code: {e}" - return (my_namespace.result,) + new_dict = {key: my_namespace.__dict__[key] for key in my_namespace.__dict__ if key not in ['__builtins__', *kwargs.keys()] and not callable(my_namespace.__dict__[key])} + + return (*new_dict.values(),) else: IDEs_DICT[unique_id].js_complete = False IDEs_DICT[unique_id].js_result = None + new_dict = {key: my_namespace.__dict__[key] for key in my_namespace.__dict__ if key not in ['__builtins__', *kwargs.keys()] and not callable(my_namespace.__dict__[key])} + PromptServer.instance.send_sync( "alekpet_js_result", - {"unique_id": unique_id, "vars": json.dumps(kwargs)}, + {"unique_id": unique_id, "vars": json.dumps(new_dict)}, ) if not run(wait_js_complete(unique_id)): print(f"IDENode_{unique_id}: Failed to get data!") else: print(f"IDENode_{unique_id}: Data received successful!") - print(IDEs_DICT[unique_id].js_result) - return (IDEs_DICT[unique_id].js_result,) + return (*IDEs_DICT[unique_id].js_result,) diff --git a/IDENode/ide_node_image.png b/IDENode/ide_node_image.png index c0feb14..bf806d4 100644 Binary files a/IDENode/ide_node_image.png and b/IDENode/ide_node_image.png differ diff --git a/IDENode/js/ide_node.js b/IDENode/js/ide_node.js index 24bc925..12c19e8 100644 --- a/IDENode/js/ide_node.js +++ b/IDENode/js/ide_node.js @@ -17,7 +17,7 @@ ace.config.set( ); // Constants -const MAX_CHAR_VARNAME = 20; +const MAX_CHAR_VARNAME = 50; const LIST_THEMES = [ "ambiance", "chaos", @@ -77,7 +77,7 @@ const runCode = () => { const date = new Date().toJSON().replace("T"," "); return date.slice(0,date.indexOf(".")) } -result = runCode();`, +result = \`Hello ComfyUI with us today \${runCode()}!\``, py: `# !!! Attention, do not insert unverified code !!! # ---- Example code ---- # Globals inputs variables: var1, var2, var3, user variables ... @@ -227,23 +227,23 @@ app.registerExtension({ }; } - node.addWidget("button", "Add Variable", "add_variable", () => { - const nameInput = node?.inputs?.length - ? `var${node.inputs.length + 1}` - : "var1"; - const varName = prompt("Enter variable name:", nameInput); - + function makeValidVariable( + varName, + textContent, + regex = /^[a-z_][a-z0-9_]*$/i + ) { if ( !varName || varName.trim() === "" || varName.length > MAX_CHAR_VARNAME || - !/^[a-z_][a-z0-9_]*$/i.test(varName) + !regex.test(varName) ) { - const windowError = createWindowModal({ + createWindowModal({ textTitle: "WARNING", textBody: [ makeElement("div", { - innerHTML: `

Variable name is incorrect!

`, + style: { fontSize: "0.7rem" }, + innerHTML: textContent, }), ], ...THEMES_MODAL_WINDOW.warning, @@ -258,12 +258,105 @@ app.registerExtension({ }, }); - return; + return false; } - const currentWidth = node.size[0]; - node.addInput(varName, "*"); - node.setSize([currentWidth, node.size[1]]); - }); + return true; + } + + node.addWidget( + "button", + "Add Input variable", + "add_input_variable", + () => { + // Input name variable and check + const nameInput = node?.inputs?.length + ? `var${node.inputs.length + 1}` + : "var1"; + + const varName = prompt( + "Enter input variable name:", + nameInput + ).trim(); + + if ( + !makeValidVariable( + varName, + `

Variable for Input name is incorrect!

` + ) + ) + return; + + // Type variable and check + let type = prompt( + "Enter type data output (default: *):", + "*" + ).trim(); + + if ( + !makeValidVariable( + type, + `

Type value is incorrect!

`, + /^[*a-z_][a-z0-9_]*$/i + ) + ) + return; + + const currentWidth = node.size[0]; + node.addInput( + `${varName}{${type === "*" ? "ANY" : type.toUpperCase()}}`, + type + ); + node.setSize([currentWidth, node.size[1]]); + } + ); + + node.addWidget( + "button", + "Add Output variable", + "add_output_variable", + () => { + const currentWidth = node.size[0]; + + // Output name variable + const nameOutput = node?.outputs?.length + ? `result${node.outputs.length + 1}` + : "result1"; + const varName = prompt( + "Enter output variable name:", + nameOutput + ).trim(); + + // Check output variable name + if ( + !makeValidVariable( + varName, + `

Variable for Output name is incorrect!

` + ) + ) + return; + + // Type variable and check + let type = prompt( + "Enter type data output (default: *):", + "*" + ).trim(); + + if ( + !makeValidVariable( + type, + `

Type value is incorrect!

`, + /^[*a-z_][a-z0-9_]*$/i + ) + ) + return; + + node.addOutput( + `${varName}{${type === "*" ? "ANY" : type.toUpperCase()}}`, + type + ); + node.setSize([currentWidth, node.size[1]]); + } + ); node.addWidget("button", "Clear", "clear_code", () => { widget.editor.setValue(""); @@ -300,7 +393,7 @@ app.registerExtension({ // Create default inputs, when first create node if (!this?.inputs) { - ["var1", "var2", "var3"].forEach((inputName) => { + ["var1{ANY}", "var2{ANY}", "var3{ANY}"].forEach((inputName) => { const currentWidth = this.size[0]; this.addInput(inputName, "*"); this.setSize([currentWidth, this.size[1]]); @@ -311,13 +404,14 @@ app.registerExtension({ // JS run api.addEventListener("alekpet_js_result", async ({ detail }) => { const { vars, unique_id } = detail; + if ((vars && !Object.keys(vars).length) || +unique_id !== this.id) { return; } await new Promise((res) => { const edit_vars = JSON.parse(vars); - let code_run = `\nlet result = null;\n`; + let code_run = ""; for (let [k, v] of Object.entries(edit_vars)) { // Check type if (v instanceof String || typeof v === "string") { @@ -328,15 +422,36 @@ app.registerExtension({ code_run += `let ${k} = ${v};\n`; } - code_run += `${widgetEditor.editor.getValue()}\nreturn result\n`; + code_run += `${widgetEditor.editor.getValue()}\nreturn [${Object.keys( + edit_vars + )},]\n`; let result_run_code = null; try { result_run_code = eval(`(function(){${code_run}}())`); } catch (e) { result_run_code = `Error in javascript code: ${e}`; console.error(`<${this.name}> ${result_run_code}`); + createWindowModal({ + textTitle: "Error in javascript code", + textBody: [ + makeElement("div", { + style: { fontSize: "0.7rem" }, + textContent: e, + }), + ], + ...THEMES_MODAL_WINDOW.error, + options: { + auto: { + autohide: true, + autoremove: true, + autoshow: true, + timewait: 2000, + }, + parent: widgetEditor.codeElement, + }, + }); + res(Object.keys(edit_vars).map((v) => e.message)); } - res(result_run_code); }).then((result_code) => { api @@ -344,7 +459,7 @@ app.registerExtension({ method: "POST", body: JSON.stringify({ unique_id: this.id.toString(), - result_code: JSON.stringify(result_code), + result_code, }), }) .then((res) => res.json()) @@ -360,7 +475,7 @@ app.registerExtension({ }); }); - this.setSize([500, 350]); + this.setSize([530, this.size[1]]); return ret; }; @@ -409,12 +524,11 @@ app.registerExtension({ nodeType.prototype.getExtraMenuOptions = function (_, options) { getExtraMenuOptions?.apply(this, arguments); - const lastSepId = options.lastIndexOf(null); const past_index = options.length - 1; const past = options[past_index]; - // options.splice(lastSepId, 0, null); if (!!past) { + // Inputs remove for (const input_idx in this.inputs) { const input = this.inputs[input_idx]; @@ -432,6 +546,26 @@ app.registerExtension({ }, }); } + + // Output remove + for (const output_idx in this.outputs) { + const output = this.outputs[output_idx]; + + if (["result{ANY}"].includes(output.name)) continue; + + options.splice(past_index + 1, 0, { + content: `Remove Output ${output.name}`, + callback: (e) => { + const currentWidth = this.size[0]; + console.log(output); + if (output.link) { + app.graph.removeLink(output.link); + } + this.removeOutput(output_idx); + this.setSize([currentWidth, this.size[1]]); + }, + }); + } } }; // end - ExtraMenuOptions diff --git a/pyproject.toml b/pyproject.toml index b13f098..64d8e2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui_custom_nodes_alekpet" description = "Nodes: PoseNode, PainterNode, TranslateTextNode, TranslateCLIPTextEncodeNode, DeepTranslatorTextNode, DeepTranslatorCLIPTextEncodeNode, ArgosTranslateTextNode, ArgosTranslateCLIPTextEncodeNode, PreviewTextNode, HexToHueNode, ColorsCorrectNode, IDENode." -version = "1.0.20" +version = "1.0.21" license = { file = "LICENSE" } [project.urls]