diff --git a/README.md b/README.md index c9a3748..3fd7650 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/simple_syrup/domain/conditioning_batch.py b/simple_syrup/domain/conditioning_batch.py index 1b69ba1..b7961b8 100644 --- a/simple_syrup/domain/conditioning_batch.py +++ b/simple_syrup/domain/conditioning_batch.py @@ -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, diff --git a/simple_syrup/domain/prompt_batch_parser.py b/simple_syrup/domain/prompt_batch_parser.py new file mode 100644 index 0000000..0169560 --- /dev/null +++ b/simple_syrup/domain/prompt_batch_parser.py @@ -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) diff --git a/simple_syrup/domain/prompt_control_prompt.py b/simple_syrup/domain/prompt_control_prompt.py index eaf3311..842fb3f 100644 --- a/simple_syrup/domain/prompt_control_prompt.py +++ b/simple_syrup/domain/prompt_control_prompt.py @@ -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"<[^>]*>" diff --git a/simple_syrup/nodes/encode_prompt_batch.py b/simple_syrup/nodes/encode_prompt_batch.py index 08654e7..5e5de55 100644 --- a/simple_syrup/nodes/encode_prompt_batch.py +++ b/simple_syrup/nodes/encode_prompt_batch.py @@ -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." + ), }, ), } diff --git a/simple_syrup/nodes/schedule_and_encode_prompts_with_prompt_control.py b/simple_syrup/nodes/schedule_and_encode_prompts_with_prompt_control.py index 1d9266e..3166d73 100644 --- a/simple_syrup/nodes/schedule_and_encode_prompts_with_prompt_control.py +++ b/simple_syrup/nodes/schedule_and_encode_prompts_with_prompt_control.py @@ -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." ), }, ), diff --git a/simple_syrup/nodes_v3/encode_prompt_batch_with_prompt_control.py b/simple_syrup/nodes_v3/encode_prompt_batch_with_prompt_control.py index 6b4eb85..bc64520 100644 --- a/simple_syrup/nodes_v3/encode_prompt_batch_with_prompt_control.py +++ b/simple_syrup/nodes_v3/encode_prompt_batch_with_prompt_control.py @@ -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=[ diff --git a/simple_syrup/nodes_v3/schedule_and_encode_prompts_with_prompt_control.py b/simple_syrup/nodes_v3/schedule_and_encode_prompts_with_prompt_control.py index ca4d234..7ba6d70 100644 --- a/simple_syrup/nodes_v3/schedule_and_encode_prompts_with_prompt_control.py +++ b/simple_syrup/nodes_v3/schedule_and_encode_prompts_with_prompt_control.py @@ -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." ), ), ], diff --git a/simple_syrup/runtime/prompt_control_schedule_encode_graph.py b/simple_syrup/runtime/prompt_control_schedule_encode_graph.py index 03aa60b..43dbb76 100644 --- a/simple_syrup/runtime/prompt_control_schedule_encode_graph.py +++ b/simple_syrup/runtime/prompt_control_schedule_encode_graph.py @@ -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]] = {} diff --git a/simple_syrup/services/prompt_batch_encoding_service.py b/simple_syrup/services/prompt_batch_encoding_service.py index 59ccb50..5a71b60 100644 --- a/simple_syrup/services/prompt_batch_encoding_service.py +++ b/simple_syrup/services/prompt_batch_encoding_service.py @@ -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 diff --git a/tests/test_conditioning_batch.py b/tests/test_conditioning_batch.py index 0ec9a6a..dccf7ee 100644 --- a/tests/test_conditioning_batch.py +++ b/tests/test_conditioning_batch.py @@ -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.""" diff --git a/tests/test_encode_prompt_batch_node.py b/tests/test_encode_prompt_batch_node.py index fc1e462..9f561bc 100644 --- a/tests/test_encode_prompt_batch_node.py +++ b/tests/test_encode_prompt_batch_node.py @@ -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: diff --git a/tests/test_encode_prompt_batch_with_prompt_control_node.py b/tests/test_encode_prompt_batch_with_prompt_control_node.py index 8bb31d5..964bdca 100644 --- a/tests/test_encode_prompt_batch_with_prompt_control_node.py +++ b/tests/test_encode_prompt_batch_with_prompt_control_node.py @@ -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() diff --git a/tests/test_prompt_batch_parser.py b/tests/test_prompt_batch_parser.py new file mode 100644 index 0000000..84e1d1a --- /dev/null +++ b/tests/test_prompt_batch_parser.py @@ -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", "") diff --git a/tests/test_prompt_control_batch_graph.py b/tests/test_prompt_control_batch_graph.py index d901adb..4512e81 100644 --- a/tests/test_prompt_control_batch_graph.py +++ b/tests/test_prompt_control_batch_graph.py @@ -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: diff --git a/tests/test_prompt_control_schedule_encode_graph.py b/tests/test_prompt_control_schedule_encode_graph.py index e4659bf..f11e9a0 100644 --- a/tests/test_prompt_control_schedule_encode_graph.py +++ b/tests/test_prompt_control_schedule_encode_graph.py @@ -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: diff --git a/tests/test_prompt_control_segment_planning_service.py b/tests/test_prompt_control_segment_planning_service.py index 4b62549..54483ea 100644 --- a/tests/test_prompt_control_segment_planning_service.py +++ b/tests/test_prompt_control_segment_planning_service.py @@ -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 [SEP|Ground] field " + ), + 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] == [ + "", + "", + "", + ] + + def test_planner_preserves_empties_and_fills_missing_side_from_global() -> None: """Authored empties remain while missing negative positions reuse global text.""" diff --git a/tests/test_schedule_and_encode_prompts_with_prompt_control_node.py b/tests/test_schedule_and_encode_prompts_with_prompt_control_node.py index 3f499bd..bba9d1d 100644 --- a/tests/test_schedule_and_encode_prompts_with_prompt_control_node.py +++ b/tests/test_schedule_and_encode_prompts_with_prompt_control_node.py @@ -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(