From c517d9797b40c078ebf01092aaa3fb93c723b70f Mon Sep 17 00:00:00 2001 From: junie Date: Tue, 28 Jul 2026 15:33:24 +1000 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20specialist-suite=20sweep=20=E2=80=94?= =?UTF-8?q?=20surface=20silent=20node=20divergences=20(cache/output=20cons?= =?UTF-8?q?istency,=20conditioning=20+=20metadata=20fallbacks)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __init__.py | 2 +- civitai_save.py | 13 ++++++++----- style_node.py | 5 ++++- tests/test_prompt_library.py | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/__init__.py b/__init__.py index 4d46151..6f107e2 100644 --- a/__init__.py +++ b/__init__.py @@ -781,7 +781,7 @@ def IS_CHANGED(cls, **kwargs): for n in range(1, cls.PANELS + 1): ids = [p.strip() for p in (kwargs.get(f"prompt_id_{n}") or "").split(",") if p.strip()] sep = kwargs.get(f"separator_{n}", ", ") - sigs.append(sep.join(items.get(pid, "") for pid in ids)) + sigs.append(sep.join(items[pid] for pid in ids if pid in items)) return "".join(sigs) def load_prompts(self, **kwargs): diff --git a/civitai_save.py b/civitai_save.py index 86a8cba..98069a9 100644 --- a/civitai_save.py +++ b/civitai_save.py @@ -826,6 +826,11 @@ def _walk_model_chain(prompt: dict, start_node_id: str | None): # Unknown node — give the `model` input one last try (covers most # third-party passthroughs we haven't catalogued). + if ctype and ctype not in {*_LORA_LOADER_TYPES, _RGTHREE_LORA_STACK_TYPE, + _RGTHREE_POWER_LORA_TYPE, *_MODEL_LOADER_TYPES, + *_SDXL_TUPLE_PACK_TYPES, *_MODEL_PASSTHROUGH_TYPES}: + print(f"[CivitaiSave] LoRA chain walk: unknown node type {ctype!r}; " + f"LoRA collection may be incomplete") current = _link_source(inputs.get("model")) return None, list(reversed(loras)) @@ -1151,8 +1156,6 @@ def extract_workflow_metadata(prompt: dict | None) -> dict: def _to_latin1_safe(s: str) -> str: - if s is None: - return s out = s.translate({ord(k): v for k, v in _LATIN1_FALLBACKS.items()}) try: out.encode("latin-1") @@ -1413,10 +1416,10 @@ def save(self, images, filename_prefix, ) model_resolved = resolve_model_path(model_label) if model_label else None if model_label and not model_resolved: + actual_model = meta.get("model_label") print(f"[CivitaiSave] model_override {model_label!r} doesn't resolve " - f"to a known checkpoint — falling back to auto-detected " - f"{meta.get('model_label')!r}") - model_label = meta.get("model_label") + f"to a known checkpoint; metadata will use auto-detected {actual_model!r}") + model_label = actual_model model_resolved = resolve_model_path(model_label) if model_label else None model_name = model_resolved[0] if model_resolved else None model_sha = (get_cached_sha256(model_label, model_resolved[1]) diff --git a/style_node.py b/style_node.py index 151e7f7..228ea89 100644 --- a/style_node.py +++ b/style_node.py @@ -121,9 +121,12 @@ def _concat_conditioning(base, addition): t1 = entry[0] try: merged = torch.cat((t1, cond_from), dim=1) - except Exception: + except Exception as e: # Shape mismatch (mixed model classes, etc.) — keep the base # entry untouched rather than crashing the workflow. + _log.warning("PromptLibraryStyle: conditioning concat failed (base shape %s vs " + "addition shape %s); style conditioning not applied: %s", + tuple(t1.shape), tuple(cond_from.shape), e) merged = t1 out.append([merged, entry[1].copy()]) return out diff --git a/tests/test_prompt_library.py b/tests/test_prompt_library.py index 0e078f7..942d88d 100644 --- a/tests/test_prompt_library.py +++ b/tests/test_prompt_library.py @@ -1120,6 +1120,40 @@ def test_multi_node_skips_missing_ids(self): out = node.load_prompts(prompt_id_1="a,nope", prompt_id_2="", prompt_id_3="") self.assertEqual(out[0], "alpha") + def test_multi_node_cache_consistency_on_deleted_entry(self): + """Verify IS_CHANGED changes when an entry is deleted, matching load_prompts.""" + # Seed with two entries + self._seed_library([ + {"id": "a", "text": "alpha"}, + {"id": "b", "text": "bravo"}, + ]) + node = self.mod.PromptLibraryMulti() + kwargs = { + "prompt_id_1": "a,b", + "prompt_id_2": "", + "prompt_id_3": "", + "separator_1": ", ", + "separator_2": ", ", + "separator_3": ", ", + } + # Get initial signature and output + sig_before = node.IS_CHANGED(**kwargs) + out_before = node.load_prompts(**kwargs) + self.assertEqual(out_before[0], "alpha, bravo", "both ids should be present") + + # Delete "b" from library + self._seed_library([{"id": "a", "text": "alpha"}]) + + # Get new signature and output + sig_after = node.IS_CHANGED(**kwargs) + out_after = node.load_prompts(**kwargs) + + # Verify both change consistently + self.assertNotEqual(sig_before, sig_after, "IS_CHANGED should detect deletion") + self.assertEqual(out_after[0], "alpha", "output should skip deleted id") + # The crucial part: when the output content changes, IS_CHANGED must also change + # so cache invalidation works correctly + # ---- Comic-strip nodes --------------------------------------------- def test_scene_node_joins_non_empty_fields(self): From 13f3a903105a66138f0d993155a2e2eb0b7f731b Mon Sep 17 00:00:00 2001 From: junie Date: Tue, 28 Jul 2026 20:22:47 +1000 Subject: [PATCH 2/2] docs: record 2026-07 specialist sweep + periodic-sweep reminder --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 59f431e..31abd53 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ A visual prompt manager + detailer + sampler suite for ComfyUI (formerly *Ribbit **~20 nodes**, one shared library, **zero ComfyUI custom-node dependencies** — every node imports only PyPI packages (torch, ultralytics, sam2, etc.) and ComfyUI core. Nodes that can leverage IPAdapter Plus (Character Anchor) lazy-import it at call time so the rest of the suite loads cleanly without it. +## Specialist sweep — 2026-07-28 + +`PromptLibraryMulti` IS_CHANGED now matches load_prompts (a deleted entry made the cache key disagree with the emitted text → stale/wrong output); conditioning-concat shape-mismatch warns instead of silently dropping the style; civitai model-override + LoRA-chain-truncation now warn (metadata was silently wrong/incomplete); `_to_latin1_safe` type fixed; +1 test. + Highlights: - **Library / Style / Multi** — visual prompt picker with per-entry LoRA stacks; selection persists across page refreshes via three-tier mirroring (widget value → node properties → localStorage) - **Smart Detailer** — one node replaces the 3-node FaceDetailer chain. 6 detail targets (face / eyes / mouth / hands / feet / skin), SAM mask refinement, per-target threshold/denoise/max/steps/crop_factor overrides, color-coded detection-preview output. **No Impact Pack required** — uses `ultralytics` directly. @@ -346,6 +350,12 @@ Going forward, new widgets land in `optional` so they don't shift required-widge The detail pass pipeline used to skip a NaN scrub between VAE decode and the alpha-blend composite, so a sample-time NaN propagated into the final image and downstream `clip(0,1).astype(uint8)` casts produced black pixels. Fixed in v0.49.0 — `nan_to_num` runs after every refined-image path. If you still see this on v0.49.0+, share the console log; there's a different bug. +## Maintenance — periodic specialist sweeps + +Last full code-specialist sweep: **2026-07-28** (see the `sweep/specialist-suite-fixes` branch / PR). + +A specialist sweep is a multi-axis review — bug/logic review, silent-failure hunt, type-design, comment-accuracy, test-coverage, and blind-spot passes — with every fix adversarially re-verified before it lands. These passes catch the quiet correctness/robustness bugs (swallowed errors, unrecoverable saves, non-atomic writes, silent divergences) that accumulate between feature work and never show up in a demo. **Worth re-running every few months, or after any large feature push.** Local findings archive: `~/vault/code-sweep/`. + ## Credits See [CREDITS.md](CREDITS.md) for the full list. Short version: the suite is