fix(nodes): fill a Pair prompt's category and tags only when the row is created (#546)
An existing prompt never changes on a run, so a cleared category stays cleared. The write now happens in the save's transaction, replacing the fill-if-empty call.
This commit is contained in:
+6
-5
@@ -2888,12 +2888,13 @@ with its own negative, a self-pair, each drift, validation); `tests/test_library
|
||||
copy, the server after, archive-only, reset to Archive) and `library-delete-concept.spec.mjs`. A Python
|
||||
change: ComfyUI needs a restart.
|
||||
|
||||
### Pair node fills the prompt's empty category and tags (#546)
|
||||
### Pair node gives a new prompt its category and tags (#546)
|
||||
|
||||
A Pair save fills the positive prompt's category and tags where each is empty and never replaces a value; a prompt
|
||||
with any tag keeps exactly those tags. The negative stays empty, the saved pair keeps its own metadata, and
|
||||
`save_prompt_pair` itself still writes no component metadata. There is no backfill: a prompt fills when a Pair node
|
||||
next executes. Tests: `tests/test_prompt_pair_node.py`. A Python change: ComfyUI needs a restart.
|
||||
A Pair save writes the category and tags to the positive prompt only when that save creates the prompt row (maintainer
|
||||
ruling). An existing prompt never changes on a run: empty, cleared or set by a person, including one first seen as
|
||||
another pair's negative. The negative stays empty, the saved pair keeps its own metadata, and `save_prompt_pair` writes
|
||||
no prompt metadata unless called with `fill_new_positive`. No backfill; optional repair is #550.
|
||||
Tests: `tests/test_prompt_pair_node.py`. A Python change: ComfyUI needs a restart.
|
||||
|
||||
### Delete… on the archived shelf (#492)
|
||||
|
||||
|
||||
+16
-40
@@ -112,8 +112,14 @@ class PairOperations:
|
||||
tags: Optional[List[str]] = None,
|
||||
rating: Optional[int] = None,
|
||||
notes: Optional[str] = None,
|
||||
fill_new_positive: bool = False,
|
||||
) -> int:
|
||||
"""Save an ordered pair atomically; repeat saves preserve its metadata."""
|
||||
"""Save an ordered pair atomically; repeat saves preserve its metadata.
|
||||
|
||||
``fill_new_positive`` also gives the positive prompt the category and
|
||||
tags, only if this save inserted its row (#546). An existing prompt
|
||||
never changes, so a cleared category stays cleared.
|
||||
"""
|
||||
with self._pair_transaction() as conn:
|
||||
return self._save_pair(
|
||||
conn,
|
||||
@@ -124,6 +130,7 @@ class PairOperations:
|
||||
tags=tags,
|
||||
rating=rating,
|
||||
notes=notes,
|
||||
fill_new_positive=fill_new_positive,
|
||||
)
|
||||
|
||||
def _save_pair(
|
||||
@@ -137,6 +144,7 @@ class PairOperations:
|
||||
tags: Optional[List[str]] = None,
|
||||
rating: Optional[int] = None,
|
||||
notes: Optional[str] = None,
|
||||
fill_new_positive: bool = False,
|
||||
) -> int:
|
||||
"""Save an ordered pair atomically; repeat saves preserve its metadata."""
|
||||
if not isinstance(positive_text, str) or not positive_text.strip():
|
||||
@@ -144,8 +152,13 @@ class PairOperations:
|
||||
if not isinstance(negative_text, str):
|
||||
raise ValueError("Negative prompt text must be a string; use '' for empty")
|
||||
# Validate metadata before any component insertion.
|
||||
_validate_metadata(title, category, tags, rating, notes)
|
||||
positive = self._pair_component(conn, positive_text)
|
||||
tags = _validate_metadata(title, category, tags, rating, notes)
|
||||
positive, created = self._pair_component_outcome(conn, positive_text)
|
||||
if created and fill_new_positive:
|
||||
conn.execute(
|
||||
"UPDATE prompts SET category = ? WHERE id = ?", (category, positive)
|
||||
)
|
||||
self._sync_prompt_tags(conn, positive, tags or [])
|
||||
negative = (
|
||||
self._pair_component(conn, negative_text) if negative_text.strip() else None
|
||||
)
|
||||
@@ -195,43 +208,6 @@ class PairOperations:
|
||||
self._sync_pair_tags(conn, pair_id, tags)
|
||||
return pair_id, True
|
||||
|
||||
def fill_empty_prompt_metadata(
|
||||
self,
|
||||
prompt_id: int,
|
||||
category: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
) -> bool:
|
||||
"""Give a prompt a category and tags where it has none (#546).
|
||||
|
||||
Each field fills on its own and never replaces a value already there.
|
||||
Tags are not additive: a prompt with any tag keeps exactly those tags.
|
||||
Returns True if the prompt changed.
|
||||
"""
|
||||
tags = _validate_metadata(None, category, tags, None, None)
|
||||
with self._pair_transaction() as conn:
|
||||
changed = False
|
||||
if category:
|
||||
changed = (
|
||||
conn.execute(
|
||||
"UPDATE prompts SET category = ? WHERE id = ? "
|
||||
"AND (category IS NULL OR TRIM(category) = '')",
|
||||
(category, prompt_id),
|
||||
).rowcount
|
||||
== 1
|
||||
)
|
||||
has_tags = conn.execute(
|
||||
"SELECT 1 FROM prompt_tags WHERE prompt_id = ? LIMIT 1", (prompt_id,)
|
||||
).fetchone()
|
||||
if tags and not has_tags:
|
||||
self._sync_prompt_tags(conn, prompt_id, tags)
|
||||
changed = True
|
||||
if changed:
|
||||
conn.execute(
|
||||
"UPDATE prompts SET updated_at = ? WHERE id = ?",
|
||||
(utc_now_iso(), prompt_id),
|
||||
)
|
||||
return changed
|
||||
|
||||
def _read_pair_rows(
|
||||
self, conn: sqlite3.Connection, where: str, params: List[Any], suffix: str = ""
|
||||
) -> List[Dict[str, Any]]:
|
||||
|
||||
+2
-10
@@ -230,19 +230,11 @@ class PromptManagerPair(PromptManagerFocused):
|
||||
negative_text,
|
||||
category=category.strip() or None,
|
||||
tags=self._parse_tags(tags),
|
||||
# The Library shows the prompt's own category and tags, not the pair's.
|
||||
fill_new_positive=True,
|
||||
)
|
||||
pair = self.db.get_prompt_pair(pair_id)
|
||||
prompt_id = pair["positive_prompt_id"]
|
||||
# The Library shows the prompt's own category and tags, not the pair's.
|
||||
# A failure here must not cost the run its count or image link.
|
||||
try:
|
||||
self.db.fill_empty_prompt_metadata(
|
||||
prompt_id,
|
||||
category=category.strip() or None,
|
||||
tags=self._parse_tags(tags),
|
||||
)
|
||||
except Exception as error:
|
||||
self.logger.warning(f"Could not fill prompt metadata: {error}")
|
||||
resolved_positive = (
|
||||
resolve_text_input(prompt, node_id, "text") if known else None
|
||||
)
|
||||
|
||||
@@ -159,11 +159,31 @@ class TestPromptPairNode(unittest.TestCase):
|
||||
self.assertEqual(positive["category"], "Mine")
|
||||
self.assertEqual(positive["tags"], ["kept"])
|
||||
|
||||
def test_category_and_tags_fill_independently_where_empty(self):
|
||||
self.save(category="Animals")
|
||||
positive, _ = self.save(negative="noise", category="Pets", tags="studio")
|
||||
self.assertEqual(positive["category"], "Animals")
|
||||
self.assertEqual(positive["tags"], ["studio"])
|
||||
def test_a_run_on_an_existing_empty_prompt_changes_nothing(self):
|
||||
self.db.save_prompt("cat", prompt_hash=generate_prompt_hash("cat"))
|
||||
before = self.db.get_prompt_by_hash(generate_prompt_hash("cat"))["updated_at"]
|
||||
positive, _ = self.save(category="Animals", tags="studio")
|
||||
self.assertIsNone(positive["category"])
|
||||
self.assertEqual(positive["tags"], [])
|
||||
self.assertEqual(positive["updated_at"], before)
|
||||
|
||||
def test_a_pair_saved_without_metadata_stays_empty_on_a_later_run(self):
|
||||
self.save()
|
||||
positive, _ = self.save(negative="noise", category="Animals", tags="studio")
|
||||
self.assertIsNone(positive["category"])
|
||||
self.assertEqual(positive["tags"], [])
|
||||
|
||||
def test_a_cleared_category_and_tags_stay_cleared(self):
|
||||
positive, _ = self.save(category="Animals", tags="studio")
|
||||
self.db.update_prompt_metadata(positive["id"], category="", tags=[])
|
||||
positive, _ = self.save(negative="noise", category="Animals", tags="studio")
|
||||
self.assertEqual(positive["category"], "")
|
||||
self.assertEqual(positive["tags"], [])
|
||||
|
||||
def test_a_prompt_first_seen_as_a_negative_stays_empty(self):
|
||||
self.save(text="dog", negative="cat")
|
||||
positive, _ = self.save(text="cat", negative="blur", category="Animals")
|
||||
self.assertIsNone(positive["category"])
|
||||
|
||||
def test_blank_widgets_leave_the_prompt_empty(self):
|
||||
positive, _ = self.save(category=" ", tags=" ")
|
||||
@@ -180,11 +200,6 @@ class TestPromptPairNode(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(positive["category"], "Animals")
|
||||
|
||||
def test_a_failed_fill_still_tracks_the_run(self):
|
||||
self.db.fill_empty_prompt_metadata = Mock(side_effect=RuntimeError("locked"))
|
||||
self.save(category="Animals")
|
||||
self.node.prompt_tracker.set_current_prompt.assert_called_once()
|
||||
|
||||
def test_library_shows_filled_metadata_without_repeating_a_tag(self):
|
||||
self.save(category="Animals", tags="studio")
|
||||
item = self.db.list_library()["items"][0]
|
||||
|
||||
Reference in New Issue
Block a user