feat(conditioning): support labeled prompt separators
This commit is contained in:
@@ -70,15 +70,15 @@ The regional node is different from the per-crop detailers. It samples one full-
|
||||
|
||||
### Per-Segment Prompt Batches
|
||||
|
||||
SimpleSyrup layers the ADetailer habit I missed from WebUI on top of the Impact-style detailer shape: writing per-segment prompt batches inline with `[SEP]`.
|
||||
SimpleSyrup layers the ADetailer habit I missed from WebUI on top of the Impact-style detailer shape: writing per-segment prompt batches inline with `[SEP]`. Separators can carry an organizational name, such as `[SEP|Sky]`; the name is discarded during parsing, and prompt-to-SEG matching remains positional.
|
||||
|
||||
Those prompts become an ordered `CONDITIONING_BATCH`, so prompt 1 stays matched to SEG 1, prompt 2 stays matched to SEG 2, and so on. This keeps the graph readable when each detected item needs its own prompt.
|
||||
|
||||
- **Encode Prompt Batch** splits prompt text with `[SEP]` and encodes ordered positive and negative `CONDITIONING_BATCH` values.
|
||||
- **Encode Prompt Batch** splits prompt text with `[SEP]` or `[SEP|name]` and encodes ordered positive and negative `CONDITIONING_BATCH` values.
|
||||
- **Conditioning Batch Start** and **Conditioning Batch Append** build ordered conditioning batches for per-segment and regional workflows.
|
||||
- **Batch Region Conditioning** combines normal `CONDITIONING` values and existing `CONDITIONING_BATCH` values into one ordered regional batch.
|
||||
- **Encode Prompt Batch w/ Prompt Control** is exported when Prompt Control is installed. It keeps the `[SEP]` batching workflow while letting Prompt Control handle prompt scheduling and encoding.
|
||||
- **Schedule & Encode Prompts** is also exported when Prompt Control is installed. It schedules Prompt Control LoRA tags, encodes positive and negative prompts, and returns normal conditioning or SimpleSyrup batches depending on whether `[SEP]` is used.
|
||||
- **Encode Prompt Batch w/ Prompt Control** is exported when Prompt Control is installed. It keeps the `[SEP]` and `[SEP|name]` batching workflow while letting Prompt Control handle prompt scheduling and encoding.
|
||||
- **Schedule & Encode Prompts** is also exported when Prompt Control is installed. It schedules Prompt Control LoRA tags, encodes positive and negative prompts, and returns normal conditioning or SimpleSyrup batches depending on whether `[SEP]` or `[SEP|name]` is used.
|
||||
|
||||
### Tile, Tag, and Guide
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, TypeAlias
|
||||
|
||||
@@ -57,15 +56,6 @@ def batch_conditioning(
|
||||
return ConditioningBatch(tuple(entries))
|
||||
|
||||
|
||||
def split_prompt_batch(text: str, separator: str = "[SEP]") -> tuple[str, ...]:
|
||||
"""Split prompt text into ordered chunks using a configurable separator."""
|
||||
|
||||
if separator == "":
|
||||
raise ValueError("separator must not be empty.")
|
||||
pattern = rf"\s*{re.escape(separator)}\s*"
|
||||
return tuple(re.split(pattern, text))
|
||||
|
||||
|
||||
def select_conditioning(
|
||||
conditioning: Conditioning | ConditioningBatch,
|
||||
index: int,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# SimpleSyrup - workflow-focused ComfyUI extensions for image generation
|
||||
# Copyright (C) 2026 Artificial Sweetener and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
"""Parse authored prompt text into ordered batch entries."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
DEFAULT_PROMPT_BATCH_SEPARATOR = "[SEP]"
|
||||
_NAMED_DEFAULT_SEPARATOR_PATTERN = r"\[SEP(?:\|[^\r\n\]]+)?\]"
|
||||
|
||||
|
||||
def split_prompt_batch(
|
||||
text: str,
|
||||
separator: str = DEFAULT_PROMPT_BATCH_SEPARATOR,
|
||||
) -> tuple[str, ...]:
|
||||
"""Split prompt text while discarding default-separator labels."""
|
||||
|
||||
if separator == "":
|
||||
raise ValueError("separator must not be empty.")
|
||||
pattern = rf"\s*(?:{_separator_pattern(separator)})\s*"
|
||||
return tuple(re.split(pattern, text))
|
||||
|
||||
|
||||
def _separator_pattern(separator: str) -> str:
|
||||
"""Return labeled default grammar or an escaped custom separator pattern."""
|
||||
|
||||
if separator == DEFAULT_PROMPT_BATCH_SEPARATOR:
|
||||
return _NAMED_DEFAULT_SEPARATOR_PATTERN
|
||||
return re.escape(separator)
|
||||
@@ -9,7 +9,7 @@ from __future__ import annotations
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .conditioning_batch import split_prompt_batch
|
||||
from .prompt_batch_parser import split_prompt_batch
|
||||
|
||||
PROMPT_TEXT_PATTERN = r"(?:^|>)([^<]+)(?=<|$)"
|
||||
LORA_TAG_PATTERN = r"<[^>]*>"
|
||||
|
||||
@@ -8,6 +8,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from ..domain.prompt_batch_parser import DEFAULT_PROMPT_BATCH_SEPARATOR
|
||||
from ..runtime.conditioning_encoding import ComfyConditioningEncoder
|
||||
from ..services.prompt_batch_encoding_service import PromptBatchEncodingService
|
||||
|
||||
@@ -24,8 +25,9 @@ class EncodePromptBatch:
|
||||
FUNCTION = "encode"
|
||||
CATEGORY = "SimpleSyrup/Conditioning"
|
||||
DESCRIPTION = (
|
||||
"Encodes [SEP]-separated prompts into matched conditioning batches, "
|
||||
"reusing each side's global prompt when a regional entry is missing."
|
||||
"Encodes prompts separated by [SEP] or [SEP|name] into matched "
|
||||
"conditioning batches, reusing each side's global prompt when a "
|
||||
"regional entry is missing."
|
||||
)
|
||||
SEARCH_ALIASES = ["conditioning batch", "prompt batch", "segs prompts"]
|
||||
|
||||
@@ -55,8 +57,9 @@ class EncodePromptBatch:
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
"tooltip": (
|
||||
"Ordered positive prompt entries separated by [SEP]; "
|
||||
"the global entry fills missing positive regions."
|
||||
"Ordered positive prompt entries separated by [SEP] "
|
||||
"or [SEP|name]; the global entry fills missing "
|
||||
"positive regions."
|
||||
),
|
||||
},
|
||||
),
|
||||
@@ -66,16 +69,21 @@ class EncodePromptBatch:
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
"tooltip": (
|
||||
"Ordered negative prompt entries separated by [SEP]; "
|
||||
"the global entry fills missing negative regions."
|
||||
"Ordered negative prompt entries separated by [SEP] "
|
||||
"or [SEP|name]; the global entry fills missing "
|
||||
"negative regions."
|
||||
),
|
||||
},
|
||||
),
|
||||
"separator": (
|
||||
"STRING",
|
||||
{
|
||||
"default": "[SEP]",
|
||||
"tooltip": "Text marker that separates prompt entries.",
|
||||
"default": DEFAULT_PROMPT_BATCH_SEPARATOR,
|
||||
"tooltip": (
|
||||
"Text marker that separates prompt entries. With the "
|
||||
"default [SEP], use [SEP|name] to add an organizational "
|
||||
"label."
|
||||
),
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
@@ -30,8 +30,9 @@ class ScheduleAndEncodePromptsWithPromptControl:
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "SimpleSyrup/Conditioning"
|
||||
DESCRIPTION = (
|
||||
"Schedules Prompt-Control LoRAs and encodes prompts. [SEP] creates "
|
||||
"matched conditioning batches using global text for missing regions."
|
||||
"Schedules Prompt-Control LoRAs and encodes prompts. [SEP] and "
|
||||
"[SEP|name] create matched conditioning batches using global text for "
|
||||
"missing regions."
|
||||
)
|
||||
SEARCH_ALIASES = ["prompt control", "schedule prompts", "encode prompts"]
|
||||
|
||||
@@ -67,8 +68,9 @@ class ScheduleAndEncodePromptsWithPromptControl:
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"tooltip": (
|
||||
"Positive Prompt-Control text; [SEP] creates ordered "
|
||||
"entries, and global text fills missing positive regions."
|
||||
"Positive Prompt-Control text; [SEP] or [SEP|name] "
|
||||
"creates ordered entries, and global text fills missing "
|
||||
"positive regions."
|
||||
),
|
||||
},
|
||||
),
|
||||
@@ -78,8 +80,9 @@ class ScheduleAndEncodePromptsWithPromptControl:
|
||||
"default": "",
|
||||
"multiline": False,
|
||||
"tooltip": (
|
||||
"Negative Prompt-Control text; [SEP] creates ordered "
|
||||
"entries, and global text fills missing negative regions."
|
||||
"Negative Prompt-Control text; [SEP] or [SEP|name] "
|
||||
"creates ordered entries, and global text fills missing "
|
||||
"negative regions."
|
||||
),
|
||||
},
|
||||
),
|
||||
|
||||
@@ -9,6 +9,7 @@ from __future__ import annotations
|
||||
from importlib import import_module
|
||||
from typing import TYPE_CHECKING, Any, ClassVar
|
||||
|
||||
from ..domain.prompt_batch_parser import DEFAULT_PROMPT_BATCH_SEPARATOR
|
||||
from ..runtime.prompt_control_batch_graph import PromptControlBatchGraphBuilder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -47,8 +48,9 @@ class EncodePromptBatchWithPromptControl(_ComfyNodeBase):
|
||||
enable_expand=True,
|
||||
category="SimpleSyrup/Conditioning",
|
||||
description=(
|
||||
"Encodes [SEP]-separated prompts into matched Prompt Control "
|
||||
"batches, reusing each side's global text for missing regions."
|
||||
"Encodes prompts separated by [SEP] or [SEP|name] into matched "
|
||||
"Prompt Control batches, reusing each side's global text for "
|
||||
"missing regions."
|
||||
),
|
||||
inputs=[
|
||||
_comfy_io.Clip.Input(
|
||||
@@ -64,9 +66,9 @@ class EncodePromptBatchWithPromptControl(_ComfyNodeBase):
|
||||
multiline=True,
|
||||
default="",
|
||||
tooltip=(
|
||||
"Positive Prompt Control prompts in positional order; each "
|
||||
"segment keeps its aligned LoRA hooks, and global text "
|
||||
"fills missing positive regions."
|
||||
"Positive Prompt Control prompts separated by [SEP] or "
|
||||
"[SEP|name] in positional order; each segment keeps its "
|
||||
"aligned LoRA hooks, and global text fills missing regions."
|
||||
),
|
||||
),
|
||||
_comfy_io.String.Input(
|
||||
@@ -74,15 +76,20 @@ class EncodePromptBatchWithPromptControl(_ComfyNodeBase):
|
||||
multiline=True,
|
||||
default="",
|
||||
tooltip=(
|
||||
"Negative Prompt Control prompts in positional order; each "
|
||||
"segment shares hooks with the matching positive index, "
|
||||
"and global text fills missing negative regions."
|
||||
"Negative Prompt Control prompts separated by [SEP] or "
|
||||
"[SEP|name] in positional order; each segment shares hooks "
|
||||
"with the matching positive index, and global text fills "
|
||||
"missing regions."
|
||||
),
|
||||
),
|
||||
_comfy_io.String.Input(
|
||||
"separator",
|
||||
default="[SEP]",
|
||||
tooltip="Text marker that splits prompts into per-SEGS entries.",
|
||||
default=DEFAULT_PROMPT_BATCH_SEPARATOR,
|
||||
tooltip=(
|
||||
"Text marker that splits prompts into per-SEGS entries. "
|
||||
"With the default [SEP], use [SEP|name] to add an "
|
||||
"organizational label."
|
||||
),
|
||||
),
|
||||
],
|
||||
outputs=[
|
||||
|
||||
@@ -49,9 +49,9 @@ class ScheduleAndEncodePromptsWithPromptControl(_ComfyNodeBase):
|
||||
enable_expand=True,
|
||||
category="SimpleSyrup/Conditioning",
|
||||
description=(
|
||||
"Schedules Prompt-Control LoRAs and encodes prompts. With [SEP], "
|
||||
"both sides are matched using global text for missing regions and "
|
||||
"share each segment's LoRA hooks."
|
||||
"Schedules Prompt-Control LoRAs and encodes prompts. With [SEP] "
|
||||
"or [SEP|name], both sides are matched using global text for "
|
||||
"missing regions and share each segment's LoRA hooks."
|
||||
),
|
||||
inputs=[
|
||||
_comfy_io.Model.Input(
|
||||
@@ -85,9 +85,9 @@ class ScheduleAndEncodePromptsWithPromptControl(_ComfyNodeBase):
|
||||
multiline=False,
|
||||
default="",
|
||||
tooltip=(
|
||||
"Positive Prompt-Control text; [SEP] creates ordered "
|
||||
"conditioning entries, and global text fills missing "
|
||||
"positive regions."
|
||||
"Positive Prompt-Control text; [SEP] or [SEP|name] creates "
|
||||
"ordered conditioning entries, and global text fills "
|
||||
"missing positive regions."
|
||||
),
|
||||
),
|
||||
_comfy_io.String.Input(
|
||||
@@ -95,9 +95,9 @@ class ScheduleAndEncodePromptsWithPromptControl(_ComfyNodeBase):
|
||||
multiline=False,
|
||||
default="",
|
||||
tooltip=(
|
||||
"Negative Prompt-Control text; [SEP] creates ordered "
|
||||
"conditioning entries, and global text fills missing "
|
||||
"negative regions."
|
||||
"Negative Prompt-Control text; [SEP] or [SEP|name] creates "
|
||||
"ordered conditioning entries, and global text fills "
|
||||
"missing negative regions."
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -8,6 +8,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from ..domain.prompt_batch_parser import DEFAULT_PROMPT_BATCH_SEPARATOR
|
||||
from ..domain.prompt_control_prompt import PreparedPromptSide, apply_encode_style
|
||||
from ..services.prompt_control_segment_planning_service import (
|
||||
PromptControlSegmentPlan,
|
||||
@@ -22,7 +23,6 @@ PROMPT_CONTROL_MISSING_MESSAGE = (
|
||||
"Schedule & Encode Prompts requires comfyui-prompt-control. "
|
||||
"Install Prompt Control or remove this node from the workflow."
|
||||
)
|
||||
PROMPT_BATCH_SEPARATOR = "[SEP]"
|
||||
|
||||
|
||||
class PromptControlScheduleEncodeGraphBuilder:
|
||||
@@ -44,7 +44,7 @@ class PromptControlScheduleEncodeGraphBuilder:
|
||||
plan = self.planning_service_class().prepare(
|
||||
positive_prompt=positive_prompt,
|
||||
negative_prompt=negative_prompt,
|
||||
separator=PROMPT_BATCH_SEPARATOR,
|
||||
separator=DEFAULT_PROMPT_BATCH_SEPARATOR,
|
||||
)
|
||||
adapter = self.graph_adapter_class.load(PROMPT_CONTROL_MISSING_MESSAGE)
|
||||
expand: dict[str, dict[str, Any]] = {}
|
||||
|
||||
@@ -8,7 +8,8 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any, Protocol
|
||||
|
||||
from ..domain.conditioning_batch import ConditioningBatch, split_prompt_batch
|
||||
from ..domain.conditioning_batch import ConditioningBatch
|
||||
from ..domain.prompt_batch_parser import split_prompt_batch
|
||||
from ..domain.prompt_segment_alignment import build_prompt_segment_alignment
|
||||
|
||||
|
||||
|
||||
@@ -12,37 +12,9 @@ from simple_syrup.domain.conditioning_batch import (
|
||||
ConditioningBatch,
|
||||
batch_conditioning,
|
||||
select_conditioning,
|
||||
split_prompt_batch,
|
||||
)
|
||||
|
||||
|
||||
def test_split_prompt_batch_without_separator_returns_single_chunk() -> None:
|
||||
"""Plain prompt text remains one prompt entry."""
|
||||
|
||||
assert split_prompt_batch("cat", "[SEP]") == ("cat",)
|
||||
|
||||
|
||||
def test_split_prompt_batch_trims_separator_whitespace() -> None:
|
||||
"""Whitespace around separators does not become prompt text."""
|
||||
|
||||
assert split_prompt_batch("cat [SEP] dog", "[SEP]") == ("cat", "dog")
|
||||
assert split_prompt_batch("cat[SEP]dog", "[SEP]") == ("cat", "dog")
|
||||
|
||||
|
||||
def test_split_prompt_batch_preserves_blank_prompt_and_empty_chunks() -> None:
|
||||
"""Blank prompts and trailing separator chunks remain explicit entries."""
|
||||
|
||||
assert split_prompt_batch("", "[SEP]") == ("",)
|
||||
assert split_prompt_batch("cat [SEP]", "[SEP]") == ("cat", "")
|
||||
|
||||
|
||||
def test_split_prompt_batch_rejects_empty_separator() -> None:
|
||||
"""An empty separator would split between every character."""
|
||||
|
||||
with pytest.raises(ValueError, match="separator must not be empty"):
|
||||
split_prompt_batch("cat", "")
|
||||
|
||||
|
||||
def test_conditioning_batch_requires_entries() -> None:
|
||||
"""A batch must contain at least one selectable entry."""
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ def test_encode_prompt_batch_contract() -> None:
|
||||
assert EncodePromptBatch.RETURN_NAMES == ("positive", "negative")
|
||||
assert EncodePromptBatch.CATEGORY == "SimpleSyrup/Conditioning"
|
||||
assert "global" in EncodePromptBatch.DESCRIPTION.lower()
|
||||
assert "[sep|name]" in EncodePromptBatch.DESCRIPTION.lower()
|
||||
assert list(inputs["required"]) == [
|
||||
"clip",
|
||||
"positive_prompt",
|
||||
@@ -36,6 +37,7 @@ def test_encode_prompt_batch_contract() -> None:
|
||||
assert inputs["required"]["positive_prompt"][1]["default"] == ""
|
||||
assert inputs["required"]["negative_prompt"][1]["default"] == ""
|
||||
assert inputs["required"]["separator"][1]["default"] == "[SEP]"
|
||||
assert "[sep|name]" in inputs["required"]["separator"][1]["tooltip"].lower()
|
||||
assert "global" in inputs["required"]["positive_prompt"][1]["tooltip"].lower()
|
||||
assert "global" in inputs["required"]["negative_prompt"][1]["tooltip"].lower()
|
||||
|
||||
@@ -60,6 +62,26 @@ def test_encode_prompt_batch_aligns_missing_negative_chunks_to_global(
|
||||
assert negative.entries == ("clip:blur", "clip:blur")
|
||||
|
||||
|
||||
def test_encode_prompt_batch_accepts_named_default_separators(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Organizational separator names do not enter encoded prompt text."""
|
||||
|
||||
monkeypatch.setattr(EncodePromptBatch, "encoder_class", _FakeEncoder)
|
||||
|
||||
positive, negative = EncodePromptBatch().encode(
|
||||
clip="clip",
|
||||
positive_prompt="global [SEP|Sky] clouds [SEP|Ground] field",
|
||||
negative_prompt="blur",
|
||||
separator="[SEP]",
|
||||
)
|
||||
|
||||
assert isinstance(positive, ConditioningBatch)
|
||||
assert isinstance(negative, ConditioningBatch)
|
||||
assert positive.entries == ("clip:global", "clip:clouds", "clip:field")
|
||||
assert negative.entries == ("clip:blur", "clip:blur", "clip:blur")
|
||||
|
||||
|
||||
def test_encode_prompt_batch_encodes_blank_and_empty_chunks(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
|
||||
@@ -21,6 +21,7 @@ def test_prompt_control_prompt_batch_node_schema() -> None:
|
||||
assert schema.enable_expand is True
|
||||
assert schema.category == "SimpleSyrup/Conditioning"
|
||||
assert "global" in schema.description.lower()
|
||||
assert "[sep|name]" in schema.description.lower()
|
||||
assert [output.io_type for output in schema.outputs] == [
|
||||
"CONDITIONING_BATCH",
|
||||
"CONDITIONING_BATCH",
|
||||
@@ -49,5 +50,6 @@ def test_prompt_control_prompt_batch_input_types() -> None:
|
||||
]
|
||||
assert inputs["required"]["clip"][0] == "CLIP"
|
||||
assert inputs["required"]["separator"][1]["default"] == "[SEP]"
|
||||
assert "[sep|name]" in inputs["required"]["separator"][1]["tooltip"].lower()
|
||||
assert "global" in inputs["required"]["positive_prompt"][1]["tooltip"].lower()
|
||||
assert "global" in inputs["required"]["negative_prompt"][1]["tooltip"].lower()
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# SimpleSyrup - workflow-focused ComfyUI extensions for image generation
|
||||
# Copyright (C) 2026 Artificial Sweetener and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
"""Test prompt-batch separator parsing behavior."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from simple_syrup.domain.prompt_batch_parser import split_prompt_batch
|
||||
|
||||
|
||||
def test_split_prompt_batch_without_separator_returns_single_chunk() -> None:
|
||||
"""Keep plain prompt text as one prompt entry."""
|
||||
|
||||
assert split_prompt_batch("cat", "[SEP]") == ("cat",)
|
||||
|
||||
|
||||
def test_split_prompt_batch_trims_separator_whitespace() -> None:
|
||||
"""Exclude whitespace surrounding separators from prompt text."""
|
||||
|
||||
assert split_prompt_batch("cat [SEP] dog", "[SEP]") == ("cat", "dog")
|
||||
assert split_prompt_batch("cat[SEP]dog", "[SEP]") == ("cat", "dog")
|
||||
|
||||
|
||||
def test_split_prompt_batch_preserves_blank_prompt_and_empty_chunks() -> None:
|
||||
"""Keep blank prompts and trailing separator chunks as explicit entries."""
|
||||
|
||||
assert split_prompt_batch("", "[SEP]") == ("",)
|
||||
assert split_prompt_batch("cat [SEP]", "[SEP]") == ("cat", "")
|
||||
|
||||
|
||||
def test_split_prompt_batch_treats_custom_separator_as_literal_text() -> None:
|
||||
"""Escape regex syntax in a configured custom separator."""
|
||||
|
||||
assert split_prompt_batch("cat .+ dog", ".+") == ("cat", "dog")
|
||||
|
||||
|
||||
def test_split_prompt_batch_accepts_named_default_separators() -> None:
|
||||
"""Discard organizational names while preserving prompt order."""
|
||||
|
||||
assert split_prompt_batch(
|
||||
"global [SEP|Sky] clouds [SEP] field [SEP|Subject 2] person",
|
||||
"[SEP]",
|
||||
) == ("global", "clouds", "field", "person")
|
||||
|
||||
|
||||
def test_split_prompt_batch_preserves_empty_chunk_after_named_separator() -> None:
|
||||
"""Keep a trailing named separator as an explicit blank prompt entry."""
|
||||
|
||||
assert split_prompt_batch("cat [SEP|Unused region]", "[SEP]") == ("cat", "")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"marker",
|
||||
(
|
||||
"[SEP|]",
|
||||
"[SEP|Sky",
|
||||
"[SEP|Sky\nClouds]",
|
||||
"[sep|Sky]",
|
||||
),
|
||||
)
|
||||
def test_split_prompt_batch_keeps_malformed_or_wrong_case_markers(
|
||||
marker: str,
|
||||
) -> None:
|
||||
"""Leave text untouched when it is not a valid default separator marker."""
|
||||
|
||||
prompt = f"cat {marker} dog"
|
||||
|
||||
assert split_prompt_batch(prompt, "[SEP]") == (prompt,)
|
||||
|
||||
|
||||
def test_split_prompt_batch_does_not_extend_custom_separator_grammar() -> None:
|
||||
"""Recognize named markers only when the configured separator is `[SEP]`."""
|
||||
|
||||
prompt = "cat [SEP|Sky] dog"
|
||||
|
||||
assert split_prompt_batch(prompt, "---") == (prompt,)
|
||||
|
||||
|
||||
def test_split_prompt_batch_rejects_empty_separator() -> None:
|
||||
"""Reject a separator that would split between every character."""
|
||||
|
||||
with pytest.raises(ValueError, match="separator must not be empty"):
|
||||
split_prompt_batch("cat", "")
|
||||
@@ -86,6 +86,25 @@ def test_prompt_control_batch_graph_builds_pack_chain_for_multiple_chunks(
|
||||
assert output.args[1] == ["BATCH.0.5.2", 0]
|
||||
|
||||
|
||||
def test_prompt_control_batch_graph_encodes_named_separator_chunks(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Build one Prompt Control encoding per named default-separator chunk."""
|
||||
|
||||
calls = _install_fake_prompt_control(monkeypatch)
|
||||
graph_utils = import_module("comfy_execution.graph_utils")
|
||||
graph_utils.GraphBuilder.set_default_prefix("NAMED", 0, 0)
|
||||
|
||||
PromptControlBatchGraphBuilder().build(
|
||||
clip=[0, 0],
|
||||
positive_prompt="global [SEP|Sky] clouds",
|
||||
negative_prompt="blur [SEP|Sky] haze",
|
||||
separator="[SEP]",
|
||||
)
|
||||
|
||||
assert [call["text"] for call in calls] == ["global", "clouds", "blur", "haze"]
|
||||
|
||||
|
||||
def test_prompt_control_batch_graph_attaches_segment_local_lora_hooks(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
|
||||
@@ -96,6 +96,30 @@ def test_schedule_encode_graph_packs_both_sides_to_matched_segment_counts(
|
||||
]
|
||||
|
||||
|
||||
def test_schedule_encode_graph_uses_named_default_separators(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Recognize named separators on the fixed-default scheduling path."""
|
||||
|
||||
calls = _install_fake_prompt_control(monkeypatch)
|
||||
|
||||
PromptControlScheduleEncodeGraphBuilder().build(
|
||||
model=["model", 0],
|
||||
clip=["clip", 0],
|
||||
positive_prompt="global [SEP|Sky] clouds [SEP|Ground] field",
|
||||
negative_prompt="blur",
|
||||
)
|
||||
|
||||
assert [call["text"] for call in calls["encode"]] == [
|
||||
"global",
|
||||
"clouds",
|
||||
"field",
|
||||
"blur",
|
||||
"blur",
|
||||
"blur",
|
||||
]
|
||||
|
||||
|
||||
def test_schedule_encode_graph_matches_lora_region_on_both_cfg_sides(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
|
||||
@@ -38,6 +38,30 @@ def test_planner_combines_lora_tags_only_at_aligned_indexes() -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_planner_aligns_lora_hooks_across_named_separators() -> None:
|
||||
"""Treat separator names as comments while aligning segment-local hooks."""
|
||||
|
||||
plan = PromptControlSegmentPlanningService().prepare(
|
||||
positive_prompt=(
|
||||
"global [SEP|Sky] clouds <lora:sky:1> [SEP|Ground] field <lora:ground:0.5>"
|
||||
),
|
||||
negative_prompt="bad [SEP|Sky] haze",
|
||||
separator="[SEP]",
|
||||
)
|
||||
|
||||
assert [chunk.text for chunk in plan.positive.chunks] == [
|
||||
"global",
|
||||
"clouds ",
|
||||
"field ",
|
||||
]
|
||||
assert [chunk.text for chunk in plan.negative.chunks] == ["bad", "haze", "bad"]
|
||||
assert [hook.lora_tags for hook in plan.hooks] == [
|
||||
"",
|
||||
"<lora:sky:1>",
|
||||
"<lora:ground:0.5>",
|
||||
]
|
||||
|
||||
|
||||
def test_planner_preserves_empties_and_fills_missing_side_from_global() -> None:
|
||||
"""Authored empties remain while missing negative positions reuse global text."""
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ def test_legacy_schedule_and_encode_prompt_control_node_contract() -> None:
|
||||
)
|
||||
assert LegacyScheduleAndEncode.FUNCTION == "execute"
|
||||
assert LegacyScheduleAndEncode.CATEGORY == "SimpleSyrup/Conditioning"
|
||||
assert "[sep|name]" in LegacyScheduleAndEncode.DESCRIPTION.lower()
|
||||
assert list(inputs["required"]) == [
|
||||
"model",
|
||||
"clip",
|
||||
@@ -46,6 +47,7 @@ def test_legacy_schedule_and_encode_prompt_control_node_contract() -> None:
|
||||
assert inputs["optional"]["encode_style"][1]["forceInput"] is True
|
||||
assert inputs["required"]["positive_prompt"][1]["multiline"] is False
|
||||
assert inputs["required"]["negative_prompt"][1]["multiline"] is False
|
||||
assert "[sep|name]" in inputs["required"]["positive_prompt"][1]["tooltip"].lower()
|
||||
|
||||
|
||||
def test_legacy_schedule_and_encode_prompt_control_execute_delegates(
|
||||
@@ -141,6 +143,7 @@ def test_schedule_and_encode_prompt_control_node_schema() -> None:
|
||||
assert schema.enable_expand is True
|
||||
assert schema.category == "SimpleSyrup/Conditioning"
|
||||
assert "global" in schema.description.lower()
|
||||
assert "[sep|name]" in schema.description.lower()
|
||||
assert [output.io_type for output in schema.outputs] == [
|
||||
"MODEL",
|
||||
"CONDITIONING,CONDITIONING_BATCH",
|
||||
@@ -189,6 +192,7 @@ def test_schedule_and_encode_prompt_control_input_types() -> None:
|
||||
assert inputs["optional"]["encode_style"][1]["forceInput"] is True
|
||||
assert inputs["required"]["positive_prompt"][1]["multiline"] is False
|
||||
assert inputs["required"]["negative_prompt"][1]["multiline"] is False
|
||||
assert "[sep|name]" in inputs["required"]["positive_prompt"][1]["tooltip"].lower()
|
||||
|
||||
|
||||
def test_schedule_and_encode_prompt_control_execute_delegates(
|
||||
|
||||
Reference in New Issue
Block a user