Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 34 additions & 30 deletions nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import shutil
import subprocess
import tempfile
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple, Union

import folder_paths

Expand Down Expand Up @@ -980,12 +980,13 @@ def INPUT_TYPES(cls):
},
),
"lm_top_k": (
"INT",
"STRING",
{
"default": 0,
"min": 0,
"max": 1000,
"tooltip": "LM top-k sampling. 0 disables hard top-k (top_p still applies).",
"default": "0",
"tooltip": (
"LM top-k sampling. 0 disables hard top-k (top_p still applies). "
"Accepts integers. Leave empty to use the default (0)."
),
},
),
"lm_negative_prompt": (
Expand Down Expand Up @@ -1026,28 +1027,24 @@ def INPUT_TYPES(cls):
},
),
"repainting_start": (
"FLOAT",
"STRING",
{
"default": -1.0,
"min": -1.0,
"max": 600.0,
"step": 0.5,
"default": "-1.0",
"tooltip": (
"Repaint region start in seconds (requires src_audio). "
"-1 = inactive (0s when repaint_end is set)."
"-1 = inactive (0s when repaint_end is set). "
"Accepts floats. Leave empty to use the default (-1.0)."
),
},
),
"repainting_end": (
"FLOAT",
"STRING",
{
"default": -1.0,
"min": -1.0,
"max": 600.0,
"step": 0.5,
"default": "-1.0",
"tooltip": (
"Repaint region end in seconds (requires src_audio). "
"-1 = inactive (source duration when repaint_start is set)."
"-1 = inactive (source duration when repaint_start is set). "
"Accepts floats. Leave empty to use the default (-1.0)."
),
},
),
Expand Down Expand Up @@ -1136,21 +1133,28 @@ def INPUT_TYPES(cls):
def VALIDATE_INPUTS(
cls,
lm_top_p=0.9,
lm_top_k=0,
lm_top_k="0",
audio_cover_strength=0.5,
repainting_start=-1.0,
repainting_end=-1.0,
repainting_start="-1.0",
repainting_end="-1.0",
**kwargs,
):
"""Allow older workflows that store numeric inputs as empty strings.
"""Validate numeric optional inputs and reject non-parseable strings.

ComfyUI skips its own type-conversion check for any input whose name
appears in this method's signature, passing the raw value directly to
``generate()`` instead. ``_coerce_float`` / ``_coerce_int`` then
``lm_top_k``, ``repainting_start``, and ``repainting_end`` are declared
as STRING in INPUT_TYPES so that ComfyUI's validation layer accepts the
empty string ``""`` that older serialised workflows may store for those
fields (``str("")`` succeeds where ``int("")`` / ``float("")`` would
raise). ``_coerce_int`` / ``_coerce_float`` inside ``generate()``
convert ``""`` to the numeric default at runtime.

Empty strings are intentionally allowed — only non-empty strings that
cannot be parsed as the expected numeric type are rejected.
``lm_top_p`` and ``audio_cover_strength`` remain FLOAT widgets; they
are listed here so that any non-empty string that cannot be parsed as
a float is caught and reported before the node runs.

Note: ComfyUI's VALIDATE_INPUTS bypasses *range* (min/max) checking
for listed inputs; it does **not** bypass type conversion — that is
why the three new fields use STRING rather than INT/FLOAT.
"""
for name, val, coerce in (
("lm_top_p", lm_top_p, float),
Expand Down Expand Up @@ -1184,12 +1188,12 @@ def generate(
lm_temperature: float = 0.85,
lm_cfg_scale: float = 2.0,
lm_top_p: float = 0.9,
lm_top_k: int = 0,
lm_top_k: Union[str, int] = "0",
lm_negative_prompt: str = "",
use_cot_caption: bool = True,
audio_cover_strength: float = 0.5,
repainting_start: float = -1.0,
repainting_end: float = -1.0,
repainting_start: Union[str, float] = "-1.0",
repainting_end: Union[str, float] = "-1.0",
lego: str = "",
src_audio: str = "",
lora_path: str = "",
Expand Down
36 changes: 33 additions & 3 deletions tests/test_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,30 @@ def test_optional_has_new_params(self):
assert "repainting_end" in opt
assert "lego" in opt

def test_lm_top_k_is_string_type(self):
"""lm_top_k must be STRING so ComfyUI never runs int('') on stale workflows."""
opt = nodes.AcestepCPPGenerate.INPUT_TYPES()["optional"]
assert opt["lm_top_k"][0] == "STRING", (
"lm_top_k must be STRING (not INT) so that empty-string widget values "
"from older workflows do not trigger ComfyUI's int('') coercion error"
)

def test_repainting_start_is_string_type(self):
"""repainting_start must be STRING so ComfyUI never runs float('') on stale workflows."""
opt = nodes.AcestepCPPGenerate.INPUT_TYPES()["optional"]
assert opt["repainting_start"][0] == "STRING", (
"repainting_start must be STRING (not FLOAT) so that empty-string widget "
"values from older workflows do not trigger ComfyUI's float('') coercion error"
)

def test_repainting_end_is_string_type(self):
"""repainting_end must be STRING so ComfyUI never runs float('') on stale workflows."""
opt = nodes.AcestepCPPGenerate.INPUT_TYPES()["optional"]
assert opt["repainting_end"][0] == "STRING", (
"repainting_end must be STRING (not FLOAT) so that empty-string widget "
"values from older workflows do not trigger ComfyUI's float('') coercion error"
)

def test_guidance_scale_default_is_zero(self):
"""guidance_scale default must be 0.0 (auto-resolved by the binary)."""
opt = nodes.AcestepCPPGenerate.INPUT_TYPES()["optional"]
Expand Down Expand Up @@ -323,9 +347,15 @@ def test_lego_tracks_list(self):
# ===========================================================================

class TestValidateInputs:
"""VALIDATE_INPUTS must accept empty strings for all numeric optional fields
so that older workflows serialised with '' instead of the numeric default
pass ComfyUI's prompt-validation stage."""
"""VALIDATE_INPUTS must accept empty strings and reject non-parseable non-empty
strings for the numeric optional fields.

``lm_top_k``, ``repainting_start``, and ``repainting_end`` are now STRING
widgets (so ComfyUI's own ``int()``/``float()`` coercion never fires on
them) while VALIDATE_INPUTS still guards against non-parseable non-empty
string values. ``lm_top_p`` and ``audio_cover_strength`` remain FLOAT;
they are in the signature so that stale empty-string values from very old
workflows can be caught with a helpful error rather than an exception."""

def _vi(self, **kwargs):
return nodes.AcestepCPPGenerate.VALIDATE_INPUTS(**kwargs)
Expand Down
47 changes: 34 additions & 13 deletions tests/test_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,21 @@ def test_generate_lm_top_p_is_numeric(self, workflow_path):
assert isinstance(wv[14], (int, float)), \
f"lm_top_p (index 14) should be numeric, got {type(wv[14])}"

def test_generate_lm_top_k_is_numeric(self, workflow_path):
"""Widget index 15 (lm_top_k) must be an integer, never an empty string."""
def test_generate_lm_top_k_is_valid(self, workflow_path):
"""Widget index 15 (lm_top_k) must be a non-empty parseable integer string."""
wf = _load(workflow_path)
for node in _nodes_by_type(wf, "AcestepCPPGenerate"):
wv = node.get("widgets_values", [])
if len(wv) > 15:
assert isinstance(wv[15], int), \
f"lm_top_k (index 15) should be int, got {type(wv[15])}: {wv[15]!r}"
val = wv[15]
assert val != "", \
f"lm_top_k (index 15) must not be an empty string, got {val!r}"
try:
int(str(val))
except (ValueError, TypeError):
raise AssertionError(
f"lm_top_k (index 15) must be parseable as int, got {val!r}"
)

def test_generate_audio_cover_strength_is_numeric(self, workflow_path):
"""Widget index 18 (audio_cover_strength) must be a number."""
Expand All @@ -161,23 +168,37 @@ def test_generate_audio_cover_strength_is_numeric(self, workflow_path):
assert isinstance(wv[18], (int, float)), \
f"audio_cover_strength (index 18) should be numeric, got {type(wv[18])}"

def test_generate_repainting_start_is_numeric(self, workflow_path):
"""Widget index 19 (repainting_start) must be a number, never an empty string."""
def test_generate_repainting_start_is_valid(self, workflow_path):
"""Widget index 19 (repainting_start) must be a non-empty parseable float string."""
wf = _load(workflow_path)
for node in _nodes_by_type(wf, "AcestepCPPGenerate"):
wv = node.get("widgets_values", [])
if len(wv) > 19:
assert isinstance(wv[19], (int, float)), \
f"repainting_start (index 19) should be numeric, got {type(wv[19])}: {wv[19]!r}"

def test_generate_repainting_end_is_numeric(self, workflow_path):
"""Widget index 20 (repainting_end) must be a number, never an empty string."""
val = wv[19]
assert val != "", \
f"repainting_start (index 19) must not be an empty string, got {val!r}"
try:
float(str(val))
except (ValueError, TypeError):
raise AssertionError(
f"repainting_start (index 19) must be parseable as float, got {val!r}"
)

def test_generate_repainting_end_is_valid(self, workflow_path):
"""Widget index 20 (repainting_end) must be a non-empty parseable float string."""
wf = _load(workflow_path)
for node in _nodes_by_type(wf, "AcestepCPPGenerate"):
wv = node.get("widgets_values", [])
if len(wv) > 20:
assert isinstance(wv[20], (int, float)), \
f"repainting_end (index 20) should be numeric, got {type(wv[20])}: {wv[20]!r}"
val = wv[20]
assert val != "", \
f"repainting_end (index 20) must not be an empty string, got {val!r}"
try:
float(str(val))
except (ValueError, TypeError):
raise AssertionError(
f"repainting_end (index 20) must be parseable as float, got {val!r}"
)

def test_generate_src_audio_is_string(self, workflow_path):
"""Widget index 22 (src_audio) must be a string."""
Expand Down
8 changes: 4 additions & 4 deletions workflow-examples/acestep-cpp-cover.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,12 @@
0.85,
2.0,
0.9,
0,
"0",
"",
true,
0.5,
-1.0,
-1.0,
"-1.0",
"-1.0",
"",
"",
"",
Expand Down Expand Up @@ -258,4 +258,4 @@
]
},
"version": 0.4
}
}
8 changes: 4 additions & 4 deletions workflow-examples/acestep-cpp-lora.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,12 @@
0.85,
2.0,
0.9,
0,
"0",
"",
true,
0.5,
-1.0,
-1.0,
"-1.0",
"-1.0",
"",
"",
"",
Expand Down Expand Up @@ -259,4 +259,4 @@
]
},
"version": 0.4
}
}
8 changes: 4 additions & 4 deletions workflow-examples/acestep-cpp-reference-audio.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,12 @@
0.85,
2.0,
0.9,
0,
"0",
"",
true,
0.5,
-1.0,
-1.0,
"-1.0",
"-1.0",
"",
"",
"",
Expand Down Expand Up @@ -258,4 +258,4 @@
]
},
"version": 0.4
}
}
8 changes: 4 additions & 4 deletions workflow-examples/acestep-cpp-text2music.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,12 @@
0.85,
2.0,
0.9,
0,
"0",
"",
true,
0.5,
-1.0,
-1.0,
"-1.0",
"-1.0",
"",
"",
"",
Expand Down Expand Up @@ -218,4 +218,4 @@
]
},
"version": 0.4
}
}
Loading