Compare commits

...

3 Commits

Author SHA1 Message Date
Jukka Seppänen
5fea7f7fa8
Merge 8982726f6d into 95f6652ef5 2026-05-10 04:15:31 -04:00
kijai
8982726f6d Cleanup nodes 2026-05-10 11:15:22 +03:00
LaVie024
95f6652ef5
Add Boolean support to Math Expression Node (#13224)
Some checks failed
Python Linting / Run Ruff (push) Waiting to run
Python Linting / Run Pylint (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Waiting to run
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Waiting to run
Execution Tests / test (macos-latest) (push) Waiting to run
Execution Tests / test (ubuntu-latest) (push) Waiting to run
Execution Tests / test (windows-latest) (push) Waiting to run
Test server launches without errors / test (push) Waiting to run
Unit Tests / test (macos-latest) (push) Waiting to run
Unit Tests / test (ubuntu-latest) (push) Waiting to run
Unit Tests / test (windows-2022) (push) Waiting to run
Generate Pydantic Stubs from api.comfy.org / generate-models (push) Has been cancelled
* Add Boolean support to math expressions

* Change boolean result test to assert values

---------

Co-authored-by: Alexis Rolland <alexisrolland@hotmail.com>
2026-05-10 15:33:47 +08:00
3 changed files with 30 additions and 45 deletions

View File

@ -14,7 +14,8 @@ class EmptyHiDreamO1LatentImage(io.ComfyNode):
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="EmptyHiDreamO1LatentImage",
category="latent/hidream_o1",
display_name="Empty HiDream-O1 Latent Image",
category="latent/image",
description=(
"Empty pixel-space latent for HiDream-O1-Image. When "
"snap_to_predefined is on, dimensions are matched (by aspect "
@ -40,7 +41,7 @@ class EmptyHiDreamO1LatentImage(io.ComfyNode):
@classmethod
def execute(cls, *, width: int, height: int, batch_size: int = 1,
snap_to_predefined: bool = True) -> io.NodeOutput:
if snap_to_predefined:
if snap_to_predefined: #TODO: better way to handle this
sw, sh = find_closest_resolution(width, height)
width, height = sw, sh
width = (width // 32) * 32
@ -53,24 +54,17 @@ class EmptyHiDreamO1LatentImage(io.ComfyNode):
class HiDreamO1ReferenceImages(io.ComfyNode):
"""Attach reference images to both positive and negative conditioning.
Refs are model-level inputs, not per-prompt CONDITIONING they must ride
on both CFG branches, otherwise CFG amplifies "with-refs vs no-refs"
instead of "edit prompt vs empty prompt with same refs" and saturation
blows out.
"""
"""Attach reference images to both positive and negative conditioning."""
@classmethod
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="HiDreamO1ReferenceImages",
category="conditioning/hidream_o1",
display_name="HiDream-O1 Reference Images",
category="conditioning/image",
description=(
"Attach 1-10 reference images to BOTH positive and negative "
"conditioning for HiDream-O1 edit (K=1) or subject-driven "
"personalization (K=2..10). Refs must ride on both CFG "
"branches; this node enforces that."
"Attach 1-10 reference images to conditioning, one for edit instruction"
"or multiple for subject-driven personalization."
),
inputs=[
io.Conditioning.Input(id="positive"),
@ -96,14 +90,9 @@ class HiDreamO1ReferenceImages(io.ComfyNode):
@classmethod
def execute(cls, *, positive, negative, images: io.Autogrow.Type) -> io.NodeOutput:
# Numeric-suffix order; alphabetic sort would give image_1, image_10, image_2, ...
refs = [images[f"image_{i}"] for i in range(1, 11) if f"image_{i}" in images]
positive = node_helpers.conditioning_set_values(
positive, {"hidream_o1_ref_images": refs},
)
negative = node_helpers.conditioning_set_values(
negative, {"hidream_o1_ref_images": refs},
)
positive = node_helpers.conditioning_set_values(positive, {"hidream_o1_ref_images": refs})
negative = node_helpers.conditioning_set_values(negative, {"hidream_o1_ref_images": refs})
return io.NodeOutput(positive, negative)
@ -114,23 +103,22 @@ class HiDreamO1Sampling(io.ComfyNode):
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="HiDreamO1Sampling",
category="advanced/model/hidream_o1",
display_name="HiDream-O1 Sampling",
category="advanced/model",
description=(
"Patch HiDream-O1's sigma shift and noise scaling factor. "
"Full recipe: shift=3.0, s_noise=8.0. "
"Dev/flash recipe: shift=1.0, s_noise=7.5."
"Base model defaults: shift=3.0, s_noise=8.0. "
"Dev/flash sampler defaults: shift=1.0, s_noise=7.5."
),
inputs=[
io.Model.Input(id="model"),
io.Float.Input(
id="shift", default=3.0, min=0.0, max=100.0, step=0.01,
tooltip="Flow-match sigma shift. 3.0 for full, 1.0 for dev.",
tooltip="Flow-match sigma shift. Defaults: 3.0 for base, 1.0 for dev.",
),
io.Float.Input(
id="s_noise", default=8.0, min=0.0, max=64.0, step=0.1,
tooltip=(
"HiDream-O1 noise scale (CONST_SCALED_NOISE._s_noise). "
"8.0 for full, 7.5 for dev/flash."
tooltip=("HiDream-O1 noise scale (CONST_SCALED_NOISE). Defaults: 8.0 for base, 7.5 for dev/flash."
),
),
],
@ -160,11 +148,9 @@ class SamplerEulerFlashFlowmatch(io.ComfyNode):
def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="SamplerEulerFlashFlowmatch",
display_name="Sampler Euler Flash Flowmatch",
category="sampling/custom_sampling/samplers",
description=(
"HiDream-O1 dev/flash sampler with tunable per-step noise "
"schedule (start, end, clip_std). Wire into SamplerCustom."
),
description=("HiDream-O1 dev/flash sampler with tunable per-step noise"),
inputs=[
io.Float.Input(
id="s_noise_start", default=7.5, min=0.0, max=64.0, step=0.1,
@ -173,17 +159,13 @@ class SamplerEulerFlashFlowmatch(io.ComfyNode):
io.Float.Input(
id="s_noise_end", default=7.5, min=0.0, max=64.0, step=0.1,
tooltip=(
"Per-step noise scale at the last step. Equals "
"s_noise_start for upstream-default behaviour; differ "
"to ramp the noise across the trajectory."
"Per-step noise scale at the last step. Default: 7.5 for dev/flash. "
"Differ from s_noise_start to linearly ramp noise across steps."
),
),
io.Float.Input(
id="noise_clip_std", default=2.5, min=0.0, max=10.0, step=0.1,
tooltip=(
"Clamp per-step noise to +/- N*std. 0 disables. "
"Upstream dev recipe: 2.5."
),
tooltip=("Clamp per-step noise to +/- N*std. 0 disables.")
),
],
outputs=[io.Sampler.Output()],

View File

@ -63,7 +63,7 @@ class MathExpressionNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
autogrow = io.Autogrow.TemplateNames(
input=io.MultiType.Input("value", [io.Float, io.Int]),
input=io.MultiType.Input("value", [io.Float, io.Int, io.Boolean]),
names=list(string.ascii_lowercase),
min=1,
)
@ -82,6 +82,7 @@ class MathExpressionNode(io.ComfyNode):
outputs=[
io.Float.Output(display_name="FLOAT"),
io.Int.Output(display_name="INT"),
io.Boolean.Output(display_name="BOOL"),
],
)
@ -97,7 +98,7 @@ class MathExpressionNode(io.ComfyNode):
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)):
if not isinstance(result, (int, float)):
raise ValueError(
f"Math Expression '{expression}' must evaluate to a numeric result, "
f"got {type(result).__name__}: {result!r}"
@ -106,7 +107,7 @@ class MathExpressionNode(io.ComfyNode):
raise ValueError(
f"Math Expression '{expression}' produced a non-finite result: {result}"
)
return io.NodeOutput(float(result), int(result))
return io.NodeOutput(float(result), int(result), bool(result))
class MathExtension(ComfyExtension):

View File

@ -124,9 +124,11 @@ class TestMathExpressionExecute:
with pytest.raises(Exception, match="not defined"):
self._exec("str(a)", a=42)
def test_boolean_result_raises(self):
with pytest.raises(ValueError, match="got bool"):
self._exec("a > b", a=5, b=3)
def test_boolean_result(self):
result = self._exec("a > b", a=5, b=3)
assert result[2] is True
result = self._exec("a > b", a=3, b=5)
assert result[2] is False
def test_empty_expression_raises(self):
with pytest.raises(ValueError, match="Expression cannot be empty"):