feat(sampling): add noise inversion and composable sampler options

This commit is contained in:
Artificial Sweetener
2026-09-30 20:50:48 -04:00
parent 913cc7ba55
commit be4bd9bb16
87 changed files with 4541 additions and 310 deletions
@@ -0,0 +1,95 @@
"""Verify optional inversion widgets on implementation-backed V3 sampler nodes."""
from __future__ import annotations
from typing import Any, ClassVar
import pytest
from simple_syrup.domain.noise_inversion import NoiseInversionOptions
from simple_syrup.nodes_v3.legacy_inversion_node_adapter import (
LegacyInversionNodeV3Adapter,
)
from simple_syrup.nodes_v3.legacy_node_wrappers import (
DetailSEGSAsRegionsV3,
DetailSEGSByScaleFactorTiledDiffusionV3,
KSamplerExtrasV3,
)
class RecordingImplementation:
"""Expose a minimal maintained declaration and the execution boundary."""
RETURN_TYPES = ("STRING",)
OUTPUT_TOOLTIPS = ("Result.",)
FUNCTION = "run"
CATEGORY = "SimpleSyrup/Test"
DESCRIPTION = "Records sampling inputs."
INPUT_IS_LIST = False
@classmethod
def INPUT_TYPES(cls) -> dict[str, Any]:
"""Keep the persisted input contract independent of inversion widgets."""
return {"required": {"text": ("STRING", {"default": "", "tooltip": "Text."})}}
def run(
self, text: object, noise_inversion: NoiseInversionOptions | None = None
) -> tuple[object, NoiseInversionOptions | None]:
"""Return delegated inputs without invoking Comfy neural execution."""
return text, noise_inversion
class RecordingAdapter(LegacyInversionNodeV3Adapter):
"""Use the production schema and list-mode normalization implementation."""
LEGACY_NODE_CLASS: ClassVar[type[Any]] = RecordingImplementation
NODE_ID = "SimpleSyrup.Recording"
DISPLAY_NAME = "Recording"
@pytest.mark.parametrize("list_mode", [False, True])
def test_inversion_controls_normalize_and_default_off(
monkeypatch: pytest.MonkeyPatch,
list_mode: bool,
) -> None:
"""Preserve existing execution while constructing the accepted recipe on request."""
monkeypatch.setattr(RecordingImplementation, "INPUT_IS_LIST", list_mode)
text = ["prompt"] if list_mode else "prompt"
output, disabled = RecordingAdapter.execute(text=text)
assert output == text and disabled is None
_, enabled = RecordingAdapter.execute(
text=text,
noise_inversion_enabled=[True] if list_mode else True,
)
assert enabled == NoiseInversionOptions()
@pytest.mark.parametrize(
"controls",
[
{"inversion_steps": 0},
{"inversion_resolution_scale": 0},
{"inversion_method": "fireflow"},
{"inversion_switch_fraction": 1},
],
)
def test_invalid_selected_inversion_controls_fail(controls: dict[str, Any]) -> None:
"""Apply domain validation instead of handing malformed controls to a sampler."""
with pytest.raises(ValueError):
RecordingAdapter.execute(
text="prompt", noise_inversion_enabled=True, **controls
)
@pytest.mark.parametrize(
"node",
[KSamplerExtrasV3, DetailSEGSAsRegionsV3, DetailSEGSByScaleFactorTiledDiffusionV3],
)
def test_existing_nodes_append_optional_inversion_without_reordering(node: Any) -> None:
"""Preserve every serialized socket position before seven optional new controls."""
schema = node.define_schema()
order = node.WORKFLOW_INPUT_ORDER
assert [item.id for item in schema.inputs[: len(order)]] == list(order)
new = schema.inputs[len(order) :]
assert len(new) == 7 and all(item.optional and item.tooltip for item in new)
assert new[0].id == "noise_inversion_enabled" and new[0].default is False
@@ -8,7 +8,7 @@ from __future__ import annotations
from typing import Any, ClassVar
from simple_syrup.nodes_v3.legacy_node_wrappers import LegacyNodeV3Adapter
from simple_syrup.nodes_v3.legacy_node_adapter import LegacyNodeV3Adapter
class _FakeHidden: