Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
13 changes: 8 additions & 5 deletions civitai_save.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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])
Expand Down
5 changes: 4 additions & 1 deletion style_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions tests/test_prompt_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down