mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-03-07 10:17:31 +08:00
* feat: add EagerEval dataclass for frontend-side node evaluation Add EagerEval to the V3 API schema, enabling nodes to declare frontend-evaluated JSONata expressions. The frontend uses this to display computation results as badges without a backend round-trip. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add Math Expression node with JSONata evaluation Add ComfyMathExpression node that evaluates JSONata expressions against dynamically-grown numeric inputs using Autogrow + MatchType. Sends input context via ui output so the frontend can re-evaluate when the expression changes without a backend round-trip. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: register nodes_math.py in extras_files loader list Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address CodeRabbit review feedback - Harden EagerEval.validate with type checks and strip() for empty strings - Add _positional_alias for spreadsheet-style names beyond z (aa, ab...) - Validate JSONata result is numeric before returning - Add jsonata to requirements.txt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: remove EagerEval, scope PR to math node only Remove EagerEval dataclass from _io.py and eager_eval usage from nodes_math.py. Eager execution will be designed as a general-purpose system in a separate effort. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use TemplateNames, cap inputs at 26, improve error message Address Kosinkadink review feedback: - Switch from Autogrow.TemplatePrefix to Autogrow.TemplateNames so input slots are named a-z, matching expression variables directly - Cap max inputs at 26 (a-z) instead of 100 - Simplify execute() by removing dual-mapping hack - Include expression and result value in error message Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add unit tests for Math Expression node Add tests for _positional_alias (a-z mapping) and execute() covering arithmetic operations, float inputs, $sum(values), and error cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: replace jsonata with simpleeval for math evaluation jsonata PyPI package has critical issues: no Python 3.12/3.13 wheels, no ARM/Apple Silicon wheels, abandoned (last commit 2023), C extension. Replace with simpleeval (pure Python, 3.4M downloads/month, MIT, AST-based security). Add math module functions (sqrt, ceil, floor, log, sin, cos, tan) and variadic sum() supporting both sum(values) and sum(a, b, c). Pin version to >=1.0,<2.0. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: update tests for simpleeval migration Update JSONata syntax to Python syntax ($sum -> sum, $string -> str), add tests for math functions (sqrt, ceil, floor, sin, log10) and variadic sum(a, b, c). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: replace MatchType with MultiType inputs and dual FLOAT/INT outputs Allow mixing INT and FLOAT connections on the same node by switching from MatchType (which forces all inputs to the same type) to MultiType. Output both FLOAT and INT so users can pick the type they need. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: update tests for mixed INT/FLOAT inputs and dual outputs Add assertions for both FLOAT (result[0]) and INT (result[1]) outputs. Add test_mixed_int_float_inputs and test_mixed_resolution_scale to verify the primary use case of multiplying resolutions by a float factor. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: make expression input multiline and validate empty expression - Add multiline=True to expression input for better UX with longer expressions - Add empty expression validation with clear "Expression cannot be empty." message Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add tests for empty expression validation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address review feedback — safe pow, isfinite guard, test coverage - Wrap pow() with _safe_pow to prevent DoS via huge exponents (pow() bypasses simpleeval's safe_power guard on **) - Add math.isfinite() check to catch inf/nan before int() conversion - Add int/float converters to MATH_FUNCTIONS for explicit casting - Add "calculator" search alias - Replace _positional_alias helper with string.ascii_lowercase - Narrow test assertions and add error path + function coverage tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update requirements.txt --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Jedrzej Kosinski <kosinkadink1@gmail.com> Co-authored-by: Christian Byrne <abolkonsky.rem@gmail.com>
120 lines
3.4 KiB
Python
120 lines
3.4 KiB
Python
"""Math expression node using simpleeval for safe evaluation.
|
|
|
|
Provides a ComfyMathExpression node that evaluates math expressions
|
|
against dynamically-grown numeric inputs.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
import string
|
|
|
|
from simpleeval import simple_eval
|
|
from typing_extensions import override
|
|
|
|
from comfy_api.latest import ComfyExtension, io
|
|
|
|
|
|
MAX_EXPONENT = 4000
|
|
|
|
|
|
def _variadic_sum(*args):
|
|
"""Support both sum(values) and sum(a, b, c)."""
|
|
if len(args) == 1 and hasattr(args[0], "__iter__"):
|
|
return sum(args[0])
|
|
return sum(args)
|
|
|
|
|
|
def _safe_pow(base, exp):
|
|
"""Wrap pow() with an exponent cap to prevent DoS via huge exponents.
|
|
|
|
The ** operator is already guarded by simpleeval's safe_power, but
|
|
pow() as a callable bypasses that guard.
|
|
"""
|
|
if abs(exp) > MAX_EXPONENT:
|
|
raise ValueError(f"Exponent {exp} exceeds maximum allowed ({MAX_EXPONENT})")
|
|
return pow(base, exp)
|
|
|
|
|
|
MATH_FUNCTIONS = {
|
|
"sum": _variadic_sum,
|
|
"min": min,
|
|
"max": max,
|
|
"abs": abs,
|
|
"round": round,
|
|
"pow": _safe_pow,
|
|
"sqrt": math.sqrt,
|
|
"ceil": math.ceil,
|
|
"floor": math.floor,
|
|
"log": math.log,
|
|
"log2": math.log2,
|
|
"log10": math.log10,
|
|
"sin": math.sin,
|
|
"cos": math.cos,
|
|
"tan": math.tan,
|
|
"int": int,
|
|
"float": float,
|
|
}
|
|
|
|
|
|
class MathExpressionNode(io.ComfyNode):
|
|
"""Evaluates a math expression against dynamically-grown inputs."""
|
|
|
|
@classmethod
|
|
def define_schema(cls) -> io.Schema:
|
|
autogrow = io.Autogrow.TemplateNames(
|
|
input=io.MultiType.Input("value", [io.Float, io.Int]),
|
|
names=list(string.ascii_lowercase),
|
|
min=1,
|
|
)
|
|
return io.Schema(
|
|
node_id="ComfyMathExpression",
|
|
display_name="Math Expression",
|
|
category="math",
|
|
search_aliases=[
|
|
"expression", "formula", "calculate", "calculator",
|
|
"eval", "math",
|
|
],
|
|
inputs=[
|
|
io.String.Input("expression", default="a + b", multiline=True),
|
|
io.Autogrow.Input("values", template=autogrow),
|
|
],
|
|
outputs=[
|
|
io.Float.Output(display_name="FLOAT"),
|
|
io.Int.Output(display_name="INT"),
|
|
],
|
|
)
|
|
|
|
@classmethod
|
|
def execute(
|
|
cls, expression: str, values: io.Autogrow.Type
|
|
) -> io.NodeOutput:
|
|
if not expression.strip():
|
|
raise ValueError("Expression cannot be empty.")
|
|
|
|
context: dict = dict(values)
|
|
context["values"] = list(values.values())
|
|
|
|
result = simple_eval(expression, names=context, functions=MATH_FUNCTIONS)
|
|
# bool check must come first because bool is a subclass of int in Python
|
|
if isinstance(result, bool) or not isinstance(result, (int, float)):
|
|
raise ValueError(
|
|
f"Math Expression '{expression}' must evaluate to a numeric result, "
|
|
f"got {type(result).__name__}: {result!r}"
|
|
)
|
|
if not math.isfinite(result):
|
|
raise ValueError(
|
|
f"Math Expression '{expression}' produced a non-finite result: {result}"
|
|
)
|
|
return io.NodeOutput(float(result), int(result))
|
|
|
|
|
|
class MathExtension(ComfyExtension):
|
|
@override
|
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
|
return [MathExpressionNode]
|
|
|
|
|
|
async def comfy_entrypoint() -> MathExtension:
|
|
return MathExtension()
|