fix(ingestion): the switch walk skips negative inputs by #90's own rule
The upstream walk treated only names containing "negative" as negative, so an off node on a neg, neg_text or text_neg input blocked an enabled positive. prompt_graph gains is_negative_input (the _NEGATIVE_KEY rule, also used by follows_input) and the walk uses it. The guards accept a node with null inputs and a side whose provenance is not an object.
This commit is contained in:
@@ -271,6 +271,49 @@ class TestSaveToLibrarySteps(unittest.TestCase):
|
||||
self.assertEqual(decision.kind, "positive")
|
||||
self.assertEqual(decision.text, "SECRET OFF TEXT")
|
||||
|
||||
def test_an_off_node_on_a_negative_input_name_never_blocks(self):
|
||||
"""The walk skips negative inputs by #90's own rule (``neg``,
|
||||
``neg_text``, ``text_neg``, ``negative_text``)."""
|
||||
for key in ("neg", "neg_text", "text_neg", "negative_text"):
|
||||
with self.subTest(key=key):
|
||||
graph = {
|
||||
"1": {
|
||||
"class_type": "PromptManagerText",
|
||||
"inputs": {"text": "bad hands", "save_to_library": False},
|
||||
},
|
||||
"9": {
|
||||
"class_type": "PromptManagerText",
|
||||
"inputs": {"text": "a cat", "save_to_library": True},
|
||||
},
|
||||
"7": {
|
||||
"class_type": "PipeEncode",
|
||||
"inputs": {"pos_text": ["9", 0], key: ["1", 0]},
|
||||
},
|
||||
"4": {"class_type": "CheckpointLoaderSimple", "inputs": {}},
|
||||
"3": {
|
||||
"class_type": "KSampler",
|
||||
"inputs": {
|
||||
"model": ["4", 0],
|
||||
"positive": ["7", 0],
|
||||
"negative": ["7", 1],
|
||||
},
|
||||
},
|
||||
}
|
||||
self.assertIsNone(file_prompt._switched_off_upstream(graph))
|
||||
self.assertNotIn(
|
||||
_decide(_graph(graph)).kind, ("not_saved", "not_saved_wired")
|
||||
)
|
||||
|
||||
def test_the_guards_accept_malformed_nodes(self):
|
||||
graph = {
|
||||
"1": {"class_type": "PromptManagerText", "inputs": None},
|
||||
"2": {"class_type": "PromptManagerText"},
|
||||
}
|
||||
self.assertIsNone(file_prompt._off_text(graph, "x"))
|
||||
side = {"provenance": ["not", "a", "dict"]}
|
||||
with mock.patch.object(file_prompt, "side_opted_out", return_value=True):
|
||||
self.assertIsNone(file_prompt._side_switch(graph, side))
|
||||
|
||||
def test_the_off_text_guard_reads_a_nodes_resolved_text(self):
|
||||
graph = {
|
||||
"1": {
|
||||
|
||||
@@ -31,6 +31,7 @@ from .generation_params import (
|
||||
from .prompt_graph import (
|
||||
PROMPT_MANAGER_TYPES,
|
||||
_positive_roots,
|
||||
is_negative_input,
|
||||
metadata_opted_out,
|
||||
resolve_text,
|
||||
save_setting,
|
||||
@@ -281,7 +282,9 @@ def _side_switch(graph: Any, side: Any) -> Optional[str]:
|
||||
"""The switch outcome of the PromptManager nodes on a side's path."""
|
||||
if not isinstance(graph, dict) or not side_opted_out(graph, side):
|
||||
return None
|
||||
path = (side.get("provenance") or {}).get("path") or []
|
||||
provenance = side.get("provenance")
|
||||
path = provenance.get("path") if isinstance(provenance, dict) else None
|
||||
path = path if isinstance(path, list) else []
|
||||
nodes = {str(step.get("node_id")) for step in path if isinstance(step, dict)} & set(
|
||||
_prompt_manager_ids(graph)
|
||||
)
|
||||
@@ -337,7 +340,7 @@ def _switched_off_upstream(graph: Any) -> Optional[str]:
|
||||
found.add(node_id)
|
||||
inputs = node.get("inputs")
|
||||
for key, value in (inputs.items() if isinstance(inputs, dict) else ()):
|
||||
if key in _NOT_TEXT_INPUTS or "negative" in str(key).lower():
|
||||
if key in _NOT_TEXT_INPUTS or is_negative_input(key):
|
||||
continue
|
||||
if isinstance(value, (list, tuple)) and len(value) == 2:
|
||||
if isinstance(value[0], (str, int)) and isinstance(value[1], int):
|
||||
@@ -353,7 +356,8 @@ def _off_text(graph: Any, text: str) -> Optional[str]:
|
||||
for node_id in _prompt_manager_ids(graph):
|
||||
node_text = resolve_text(graph, node_id)
|
||||
if node_text is None:
|
||||
literal = graph[node_id].get("inputs", {}).get("text")
|
||||
inputs = graph[node_id].get("inputs")
|
||||
literal = inputs.get("text") if isinstance(inputs, dict) else None
|
||||
node_text = literal if isinstance(literal, str) else None
|
||||
if node_text is not None and node_text.strip() == wanted:
|
||||
matched.append(save_setting(graph, node_id))
|
||||
|
||||
@@ -41,9 +41,14 @@ _TEXT_KEY = re.compile(r"^(text|string|prompt)(_?[a-z0-9]+)?$", re.IGNORECASE)
|
||||
_NEGATIVE_KEY = re.compile(r"(^|_)neg(ative)?($|_)", re.IGNORECASE)
|
||||
|
||||
|
||||
def is_negative_input(key: Any) -> bool:
|
||||
"""Whether an input name carries a negative prompt (#90's one rule)."""
|
||||
return isinstance(key, str) and bool(_NEGATIVE_KEY.search(key))
|
||||
|
||||
|
||||
def follows_input(key: Any) -> bool:
|
||||
"""Whether an input name carries positive conditioning or prompt text."""
|
||||
if not isinstance(key, str) or _NEGATIVE_KEY.search(key):
|
||||
if not isinstance(key, str) or is_negative_input(key):
|
||||
return False
|
||||
return key == "positive" or bool(
|
||||
_CONDITIONING_KEY.match(key) or _TEXT_KEY.match(key)
|
||||
|
||||
Reference in New Issue
Block a user