From db94472a27fc8209c792dc0f8ee8daf4ccff9d18 Mon Sep 17 00:00:00 2001 From: Daniel Martinek Date: Thu, 26 Mar 2026 11:17:11 +0100 Subject: [PATCH] Add string node --- README.md | 2 +- more_math/StringMathNode.py | 111 ++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 more_math/StringMathNode.py diff --git a/README.md b/README.md index 2c9e83d..9acf050 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ You can also get the node from comfy manager under the name of More math. - functions and variables in math expressions - Conversion between INT and FLOAT; INT and BOOLEAN; AUDIO and IMAGE (red - real - strenght of cosine of frequency; blue - imaginary - strenght of sine of frequency; green - log1p of amplitude - just so it looks good to humans) -- Nodes for FLOAT, CONDITIONING, LATENT, IMAGE, MASK, NOISE, AUDIO, VIDEO, MODEL, CLIP, VAE, SIGMAS and GUIDER +- Nodes for FLOAT, STRING, CONDITIONING, LATENT, IMAGE, MASK, NOISE, AUDIO, VIDEO, MODEL, CLIP, VAE, SIGMAS and GUIDER - Vector Math: Support for List literals `[v1, v2, ...]` and operations between lists/scalars/tensors - Custom functions `funcname(variable,variable,...)->expression;` they can be used in any later defined custom function or in expression. Shadowing inbuilt functions do not work. **Be careful with recursion. There is no stack limit. Got to 700 000 iterations before I got bored.** - Custom variables `varname=expression;` They can be used in any later assigment or final expression. diff --git a/more_math/StringMathNode.py b/more_math/StringMathNode.py new file mode 100644 index 0000000..2054991 --- /dev/null +++ b/more_math/StringMathNode.py @@ -0,0 +1,111 @@ +from .helper_functions import ( + generate_dim_variables, + parse_expr, + getIndexTensorAlongDim, + as_tensor, + normalize_to_common_shape, + make_zero_like, + get_v_variable, + get_f_variable, + checkLazyNew +) +from .Parser.UnifiedMathVisitor import UnifiedMathVisitor +from comfy_api.latest import io +import torch +from .Stack import MrmthStack +import copy +from .ParseTree import MrmthParseTree + +class StringMathNode(io.ComfyNode): + """ + Enables math expressions on Audio. + + Inputs: + I: Autogrow image inputs (I0, I1, ...) + F: Autogrow float inputs (F0, F1, ...) + Image: Expression + """ + + @classmethod + def define_schema(cls) -> io.Schema: + return io.Schema( + node_id="mrmth_ag_AudioMathNode", + category="More math", + display_name="Audio math", + inputs=[ + io.Autogrow.Input(id="V",template=io.Autogrow.TemplatePrefix(io.String.Input("values", optional=True), prefix="V", min=1, max=50)), + io.Autogrow.Input(id="F", template=io.Autogrow.TemplatePrefix(io.Float.Input("float", default=0.0, optional=True, lazy=True, force_input=True), prefix="F", min=1, max=50)), + io.MultiType.Input( + io.String.Input("Expression", default="", multiline=False), + types=[io.String,MrmthParseTree], + tooltip="3D model file or path string", + ), + io.Int.Input(id="batching", default=0), + io.Bool.Input( + id="remember_stack", + default=False, + display_name="Remember stack across batch", + tooltip=( + "If enabled, stack is copied at output leading to changes being remembered during batch operations (node runs multiple times in sucession). If disabled each batch gets it's own copy of the stack." + ), + ), + MrmthStack.Input(id="stack", tooltip="Access stack between nodes",optional=True) + ], + outputs=[ + io.Audio.Output(is_output_list=True), + MrmthStack.Output(), + ], + ) + + @classmethod + def check_lazy_status(cls, Expression, V, F,batching=0, remember_stack=False,stack={}): + return checkLazyNew(Expression,V,F) + + + @classmethod + def execute(cls, V, F, Expression, batching=0, remember_stack=False, stack={}): + + variables = { + "a": V.get(0,""), "b": V.get(1,""), "c": V.get(2,""), "d": V.get(3,""), + "w": F.get("F0", 0.0) if F.get("F0") is not None else 0.0, + "x": F.get("F1", 0.0) if F.get("F1") is not None else 0.0, + "y": F.get("F2", 0.0) if F.get("F2") is not None else 0.0, + "z": F.get("F3", 0.0) if F.get("F3") is not None else 0.0 + } + + v_stacked, v_cnt = get_v_variable(V, length_mismatch="do nothing") + if v_stacked is not None: + variables["V"] = v_stacked + variables["Vcnt"] = float(v_cnt) + variables["V_count"] = float(v_cnt) + + f_stacked, f_cnt = get_f_variable(F) + if f_stacked is not None: + variables["F"] = f_stacked + variables["Fcnt"] = float(f_cnt) + variables["F_count"] = float(f_cnt) + + for k, val in F.items(): + variables[k] = val if val is not None else 0.0 + + tree = None + if isinstance(Expression,str): + tree = parse_expr(Expression) + else: + tree = Expression + visitor = UnifiedMathVisitor(variables, len(V[0]),torch.device("cpu"),state_storage=stack) + result = visitor.visit(tree) + result = str(result) + + if batching and batching > 0: + def chunks(s, n): + """Produce `n`-character chunks from `s`.""" + for start in range(0, len(s), n): + yield s[start:start+n] + + stack = stack if remember_stack else copy.deepcopy(stack) + return (chunks(result,batching), stack) + else: + stack = stack if remember_stack else copy.deepcopy(stack) + + return (result, stack)