From 9e87102d63db1d99c063978a3530d74867acf163 Mon Sep 17 00:00:00 2001 From: aimfordeb <133010426+aimfordeb@users.noreply.github.com> Date: Fri, 2 Jan 2026 06:11:57 +0200 Subject: [PATCH 1/9] Added new Lorebook Enhancer Node Meant to work together with Prompt Enhancer. Loads the JSON lorebook (SillyTavern format), processes your original prompt, then you can output it to Prompt Enhancer. --- funpack.py | 122 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/funpack.py b/funpack.py index 7541c2d7..4ea07314 100644 --- a/funpack.py +++ b/funpack.py @@ -20,6 +20,8 @@ import comfy.clip_vision import math import json from comfy.utils import ProgressBar +import random +import re # Constants from StoryMem IMAGE_FACTOR = 28 @@ -29,6 +31,121 @@ MAX_KEYFRAME_NUM = 3 ADAPTIVE_ALPHA = 0.01 HPSV3_QUALITY_THRESHOLD = 3.0 +class FunPackLorebookEnhancer: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "prompt": ("STRING", {"multiline": True, "default": "A passionate NSFW scene in the bedroom"}), + "lorebook_path": ("STRING", {"default": "nsfw_lorebook.json", "multiline": False}), + "scan_depth": ("INT", {"default": 1, "min": 1, "max": 10}), + }, + "optional": { + "context_history": ("STRING", {"multiline": True, "default": ""}), + } + } + + RETURN_TYPES = ("STRING", "STRING") + RETURN_NAMES = ("enhanced_prompt", "injected_details") + FUNCTION = "enhance" + CATEGORY = "FunPack/Prompt" + OUTPUT_NODE = True + + def _match_keys(self, keys, text): + if not keys: + return False + if isinstance(keys, str): + keys = [k.strip() for k in keys.split(",") if k.strip()] + for key in keys: + if key.startswith("/") and key.endswith("/"): + try: + pattern = re.compile(key[1:-1], re.IGNORECASE) + if pattern.search(text): + return True + except: + pass + elif key.lower() in text.lower(): + return True + return False + + def _match_secondary(self, secs, text, logic): + if not secs: + return True + if isinstance(secs, str): + secs = [s.strip() for s in secs.split(",") if s.strip()] + matches = [self._match_keys([s], text) for s in secs] + if logic == 0: # ANY + return any(matches) + if logic == 1: # ALL + return all(matches) + if logic == 2: # NOT ANY + return not any(matches) + if logic == 3: # NOT ALL + return not all(matches) + return True + + def enhance(self, prompt, lorebook_path, scan_depth, context_history=""): + if not os.path.exists(lorebook_path): + return (prompt, "ERROR: Lorebook file not found") + + with open(lorebook_path, 'r', encoding="utf-8") as f: + try: + lorebook = json.load(f) + except: + return (prompt, "ERROR: Invalid JSON in lorebook") + + entries = lorebook.get("entries", []) + if not entries: + return (prompt, "No entries in lorebook") + + # Prepare text to scan + scan_text = (context_history + "\n" + prompt).lower() + lines = scan_text.split("\n") + recent_lines = "\n".join(lines[-scan_depth:]) + scan_text = recent_lines + + activated = [] + for entry in entries: + if not entry.get("enabled", True): + continue + + if entry.get("constant", False): + activated.append(entry) + continue + + if not self._match_keys(entry.get("key"), scan_text): + continue + + if entry.get("selective", False): + if not self._match_secondary(entry.get("keysecondary"), scan_text, entry.get("selectiveLogic", 0)): + continue + + prob = entry.get("extensions", {}).get("probability", 100) + if random.randint(1, 100) > prob: + continue + + activated.append(entry) + + # Sort by insertion_order (higher = later = stronger recency) + activated.sort(key=lambda e: e.get("insertion_order", 0)) + + # Inject content + injected = [] + enhanced_prompt = prompt + for entry in activated: + content = entry.get("content", "").strip() + if not content: + continue + position = entry.get("position", "after_char") + if position in ["after_char", "append"]: + enhanced_prompt += "\n" + content + else: # before_char or prepend + enhanced_prompt = content + "\n" + enhanced_prompt + injected.append(content) + + injected_text = "\n\n".join(injected) if injected else "No lorebook entries triggered" + return (enhanced_prompt, injected_text) + class FunPackStoryMemJSONConverter: """ FunPack StoryMem LoRA JSON Converter - supports both manual input and Qwen-generated output. @@ -991,6 +1108,7 @@ NODE_CLASS_MAPPINGS = { "FunPackVideoStitch": FunPackVideoStitch, "FunPackContinueVideo": FunPackContinueVideo, "FunPackCreativeTemplate": FunPackCreativeTemplate, + "FunPackLorebookEnhancer": FunPackLorebookEnhancer, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -1001,7 +1119,8 @@ NODE_DISPLAY_NAME_MAPPINGS = { "FunPackPromptEnhancer": "FunPack Prompt Enhancer (Standalone)", "FunPackVideoStitch": "FunPack Video Stitch", "FunPackContinueVideo": "FunPack Continue Video", - "FunPackCreativeTemplate": "FunPack Creative Template" + "FunPackCreativeTemplate": "FunPack Creative Template", + "FunPackLorebookEnhancer": "FunPack Lorebook Enhancer" } @@ -1016,3 +1135,4 @@ NODE_DISPLAY_NAME_MAPPINGS = { + From 701f4d8ae5ab3847e0ddf1adbb643f0742ca5ada Mon Sep 17 00:00:00 2001 From: aimfordeb <133010426+aimfordeb@users.noreply.github.com> Date: Fri, 2 Jan 2026 06:15:22 +0200 Subject: [PATCH 2/9] Update funpack.py --- funpack.py | 104 ++++++++++++++++++++++++++++------------------------- 1 file changed, 55 insertions(+), 49 deletions(-) diff --git a/funpack.py b/funpack.py index 4ea07314..3e680584 100644 --- a/funpack.py +++ b/funpack.py @@ -31,17 +31,21 @@ MAX_KEYFRAME_NUM = 3 ADAPTIVE_ALPHA = 0.01 HPSV3_QUALITY_THRESHOLD = 3.0 -class FunPackLorebookEnhancer: +lass FunPackLorebookEnhancer: @classmethod def INPUT_TYPES(cls): return { "required": { "prompt": ("STRING", {"multiline": True, "default": "A passionate NSFW scene in the bedroom"}), - "lorebook_path": ("STRING", {"default": "nsfw_lorebook.json", "multiline": False}), "scan_depth": ("INT", {"default": 1, "min": 1, "max": 10}), }, "optional": { "context_history": ("STRING", {"multiline": True, "default": ""}), + # ── Multiple lorebook support ───────────────────────────────────── + "lorebook_1": ("STRING", {"default": "", "multiline": False}), + "lorebook_2": ("STRING", {"default": "", "multiline": False}), + "lorebook_3": ("STRING", {"default": "", "multiline": False}), + "lorebook_4": ("STRING", {"default": "", "multiline": False}), } } @@ -74,76 +78,77 @@ class FunPackLorebookEnhancer: if isinstance(secs, str): secs = [s.strip() for s in secs.split(",") if s.strip()] matches = [self._match_keys([s], text) for s in secs] - if logic == 0: # ANY - return any(matches) - if logic == 1: # ALL - return all(matches) - if logic == 2: # NOT ANY - return not any(matches) - if logic == 3: # NOT ALL - return not all(matches) + if logic == 0: return any(matches) + if logic == 1: return all(matches) + if logic == 2: return not any(matches) + if logic == 3: return not all(matches) return True - def enhance(self, prompt, lorebook_path, scan_depth, context_history=""): - if not os.path.exists(lorebook_path): - return (prompt, "ERROR: Lorebook file not found") - - with open(lorebook_path, 'r', encoding="utf-8") as f: - try: + def _process_lorebook(self, path, scan_text, activated): + if not path or not os.path.exists(path): + return activated + try: + with open(path, 'r', encoding="utf-8") as f: lorebook = json.load(f) - except: - return (prompt, "ERROR: Invalid JSON in lorebook") + entries = lorebook.get("entries", []) + for entry in entries: + if not entry.get("enabled", True): + continue + if entry.get("constant", False): + activated.append(entry) + continue + if not self._match_keys(entry.get("key"), scan_text): + continue + if entry.get("selective", False): + if not self._match_secondary(entry.get("keysecondary"), scan_text, entry.get("selectiveLogic", 0)): + continue + prob = entry.get("extensions", {}).get("probability", 100) + if random.randint(1, 100) > prob: + continue + activated.append(entry) + except Exception as e: + print(f"Warning: Failed to load lorebook {path}: {str(e)}") + return activated - entries = lorebook.get("entries", []) - if not entries: - return (prompt, "No entries in lorebook") - - # Prepare text to scan + def enhance(self, prompt, scan_depth, context_history="", + lorebook_1="", lorebook_2="", lorebook_3="", lorebook_4=""): + + # Prepare scan text scan_text = (context_history + "\n" + prompt).lower() lines = scan_text.split("\n") recent_lines = "\n".join(lines[-scan_depth:]) scan_text = recent_lines + # Collect all activated entries from all loaded lorebooks activated = [] - for entry in entries: - if not entry.get("enabled", True): - continue + + for path in [lorebook_1, lorebook_2, lorebook_3, lorebook_4]: + activated = self._process_lorebook(path, scan_text, activated) - if entry.get("constant", False): - activated.append(entry) - continue + if not activated: + return (prompt, "No lorebook entries triggered") - if not self._match_keys(entry.get("key"), scan_text): - continue - - if entry.get("selective", False): - if not self._match_secondary(entry.get("keysecondary"), scan_text, entry.get("selectiveLogic", 0)): - continue - - prob = entry.get("extensions", {}).get("probability", 100) - if random.randint(1, 100) > prob: - continue - - activated.append(entry) - - # Sort by insertion_order (higher = later = stronger recency) + # Sort by insertion_order (higher = inserted later = stronger influence) activated.sort(key=lambda e: e.get("insertion_order", 0)) - # Inject content + # Build enhanced prompt + collect injected text for debug injected = [] enhanced_prompt = prompt + for entry in activated: content = entry.get("content", "").strip() if not content: continue - position = entry.get("position", "after_char") - if position in ["after_char", "append"]: + + position = entry.get("position", "after_char").lower() + if position in ["after_char", "append", "bottom"]: enhanced_prompt += "\n" + content - else: # before_char or prepend + else: # before_char / prepend / top enhanced_prompt = content + "\n" + enhanced_prompt - injected.append(content) + + injected.append(f"[{entry.get('comment', 'no comment')}] {content}") - injected_text = "\n\n".join(injected) if injected else "No lorebook entries triggered" + injected_text = "\n\n".join(injected) if injected else "No matches" return (enhanced_prompt, injected_text) class FunPackStoryMemJSONConverter: @@ -1136,3 +1141,4 @@ NODE_DISPLAY_NAME_MAPPINGS = { + From dc8d56a0dcb371735427b5f256260ed2700d1518 Mon Sep 17 00:00:00 2001 From: aimfordeb <133010426+aimfordeb@users.noreply.github.com> Date: Fri, 2 Jan 2026 06:19:37 +0200 Subject: [PATCH 3/9] Update funpack.py --- funpack.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/funpack.py b/funpack.py index 3e680584..60b8042e 100644 --- a/funpack.py +++ b/funpack.py @@ -31,7 +31,7 @@ MAX_KEYFRAME_NUM = 3 ADAPTIVE_ALPHA = 0.01 HPSV3_QUALITY_THRESHOLD = 3.0 -lass FunPackLorebookEnhancer: +class FunPackLorebookEnhancer: @classmethod def INPUT_TYPES(cls): return { @@ -1142,3 +1142,4 @@ NODE_DISPLAY_NAME_MAPPINGS = { + From 0e8a6d9c8a2e8f8f2b6442f7ca6912821293f61a Mon Sep 17 00:00:00 2001 From: aimfordeb <133010426+aimfordeb@users.noreply.github.com> Date: Fri, 2 Jan 2026 06:19:51 +0200 Subject: [PATCH 4/9] Update funpack.py --- funpack.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/funpack.py b/funpack.py index 60b8042e..ab8a595a 100644 --- a/funpack.py +++ b/funpack.py @@ -36,7 +36,7 @@ class FunPackLorebookEnhancer: def INPUT_TYPES(cls): return { "required": { - "prompt": ("STRING", {"multiline": True, "default": "A passionate NSFW scene in the bedroom"}), + "prompt": ("STRING", {"multiline": True, "default": "A passionate scene in the bedroom"}), "scan_depth": ("INT", {"default": 1, "min": 1, "max": 10}), }, "optional": { @@ -1143,3 +1143,4 @@ NODE_DISPLAY_NAME_MAPPINGS = { + From 0bad55f0a523d3774f79062987004b66257f5a56 Mon Sep 17 00:00:00 2001 From: aimfordeb <133010426+aimfordeb@users.noreply.github.com> Date: Fri, 2 Jan 2026 06:20:23 +0200 Subject: [PATCH 5/9] Update funpack.py --- funpack.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/funpack.py b/funpack.py index ab8a595a..309213b5 100644 --- a/funpack.py +++ b/funpack.py @@ -52,7 +52,7 @@ class FunPackLorebookEnhancer: RETURN_TYPES = ("STRING", "STRING") RETURN_NAMES = ("enhanced_prompt", "injected_details") FUNCTION = "enhance" - CATEGORY = "FunPack/Prompt" + CATEGORY = "FunPack" OUTPUT_NODE = True def _match_keys(self, keys, text): @@ -1144,3 +1144,4 @@ NODE_DISPLAY_NAME_MAPPINGS = { + From c4ffe9679beb331cc290061487bdb9626baa97ed Mon Sep 17 00:00:00 2001 From: aimfordeb <133010426+aimfordeb@users.noreply.github.com> Date: Fri, 2 Jan 2026 06:37:20 +0200 Subject: [PATCH 6/9] Update funpack.py --- funpack.py | 127 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 81 insertions(+), 46 deletions(-) diff --git a/funpack.py b/funpack.py index 309213b5..49598988 100644 --- a/funpack.py +++ b/funpack.py @@ -32,25 +32,36 @@ ADAPTIVE_ALPHA = 0.01 HPSV3_QUALITY_THRESHOLD = 3.0 class FunPackLorebookEnhancer: + """ + Injects context from SillyTavern-style lorebook JSON files based on keywords in the prompt. + Supports multiple lorebooks, constant entries, selective filtering, probability, etc. + """ @classmethod def INPUT_TYPES(cls): return { "required": { - "prompt": ("STRING", {"multiline": True, "default": "A passionate scene in the bedroom"}), - "scan_depth": ("INT", {"default": 1, "min": 1, "max": 10}), + "prompt": ("STRING", { + "multiline": True, + "default": "The brave explorer enters an ancient forest temple at twilight, discovering glowing runes on the walls." + }), }, "optional": { - "context_history": ("STRING", {"multiline": True, "default": ""}), - # ── Multiple lorebook support ───────────────────────────────────── "lorebook_1": ("STRING", {"default": "", "multiline": False}), "lorebook_2": ("STRING", {"default": "", "multiline": False}), "lorebook_3": ("STRING", {"default": "", "multiline": False}), "lorebook_4": ("STRING", {"default": "", "multiline": False}), + "context_history": ("STRING", {"multiline": True, "default": ""}), + "scan_depth": ("INT", { + "default": 4, + "min": 1, + "max": 12, + "step": 1 + }), } } RETURN_TYPES = ("STRING", "STRING") - RETURN_NAMES = ("enhanced_prompt", "injected_details") + RETURN_NAMES = ("enhanced_prompt", "injected_content") FUNCTION = "enhance" CATEGORY = "FunPack" OUTPUT_NODE = True @@ -60,15 +71,19 @@ class FunPackLorebookEnhancer: return False if isinstance(keys, str): keys = [k.strip() for k in keys.split(",") if k.strip()] + text = text.lower() for key in keys: + key = key.strip() + if not key: + continue if key.startswith("/") and key.endswith("/"): try: pattern = re.compile(key[1:-1], re.IGNORECASE) if pattern.search(text): return True except: - pass - elif key.lower() in text.lower(): + continue + elif key.lower() in text: return True return False @@ -78,78 +93,98 @@ class FunPackLorebookEnhancer: if isinstance(secs, str): secs = [s.strip() for s in secs.split(",") if s.strip()] matches = [self._match_keys([s], text) for s in secs] - if logic == 0: return any(matches) - if logic == 1: return all(matches) - if logic == 2: return not any(matches) - if logic == 3: return not all(matches) + + if logic == 0: # ANY + return any(matches) + elif logic == 1: # ALL + return all(matches) + elif logic == 2: # NOT ANY + return not any(matches) + elif logic == 3: # NOT ALL + return not all(matches) return True def _process_lorebook(self, path, scan_text, activated): if not path or not os.path.exists(path): + print(f"[Lorebook Enhancer] File not found: {path}") return activated + try: - with open(path, 'r', encoding="utf-8") as f: + with open(path, 'r', encoding='utf-8') as f: lorebook = json.load(f) + + # Support both list and dict format for entries entries = lorebook.get("entries", []) + if isinstance(entries, dict): + entries = list(entries.values()) + for entry in entries: if not entry.get("enabled", True): continue - if entry.get("constant", False): - activated.append(entry) - continue - if not self._match_keys(entry.get("key"), scan_text): - continue - if entry.get("selective", False): - if not self._match_secondary(entry.get("keysecondary"), scan_text, entry.get("selectiveLogic", 0)): + + is_constant = entry.get("constant", False) + + if not is_constant: + if not self._match_keys(entry.get("key", []), scan_text): continue + + if entry.get("selective", False): + sec_keys = entry.get("keysecondary", []) or entry.get("secondary_keys", []) + logic = entry.get("selectiveLogic", 0) + if not self._match_secondary(sec_keys, scan_text, logic): + continue + prob = entry.get("extensions", {}).get("probability", 100) if random.randint(1, 100) > prob: continue + activated.append(entry) + + except json.JSONDecodeError as e: + print(f"[Lorebook Enhancer] JSON decode error in {path}: {str(e)}") except Exception as e: - print(f"Warning: Failed to load lorebook {path}: {str(e)}") + print(f"[Lorebook Enhancer] Failed to process {path}: {type(e).__name__}: {str(e)}") + return activated - def enhance(self, prompt, scan_depth, context_history="", - lorebook_1="", lorebook_2="", lorebook_3="", lorebook_4=""): + def enhance(self, prompt, + lorebook_1="", lorebook_2="", lorebook_3="", lorebook_4="", + context_history="", scan_depth=4): # Prepare scan text - scan_text = (context_history + "\n" + prompt).lower() - lines = scan_text.split("\n") - recent_lines = "\n".join(lines[-scan_depth:]) - scan_text = recent_lines + full_text = (context_history + "\n" + prompt).lower() + lines = full_text.splitlines() + scan_text = "\n".join(lines[-scan_depth:]) if scan_depth > 0 else full_text - # Collect all activated entries from all loaded lorebooks activated = [] - for path in [lorebook_1, lorebook_2, lorebook_3, lorebook_4]: - activated = self._process_lorebook(path, scan_text, activated) + if path.strip(): + activated = self._process_lorebook(path.strip(), scan_text, activated) if not activated: - return (prompt, "No lorebook entries triggered") + return (prompt, "No lorebook entries were triggered.") - # Sort by insertion_order (higher = inserted later = stronger influence) - activated.sort(key=lambda e: e.get("insertion_order", 0)) + activated.sort(key=lambda e: e.get("insertion_order", e.get("order", 0))) - # Build enhanced prompt + collect injected text for debug injected = [] - enhanced_prompt = prompt - + enhanced = prompt + for entry in activated: content = entry.get("content", "").strip() if not content: continue - - position = entry.get("position", "after_char").lower() - if position in ["after_char", "append", "bottom"]: - enhanced_prompt += "\n" + content - else: # before_char / prepend / top - enhanced_prompt = content + "\n" + enhanced_prompt - - injected.append(f"[{entry.get('comment', 'no comment')}] {content}") - injected_text = "\n\n".join(injected) if injected else "No matches" - return (enhanced_prompt, injected_text) + position = entry.get("position", "after_char").lower() + if position in ["after_char", "append", "bottom", "3"]: + enhanced += "\n" + content + else: + enhanced = content + "\n" + enhanced + + source = entry.get("comment") or entry.get("name") or f"uid:{entry.get('uid','?')}" or "unnamed" + injected.append(f"[{source}] {content}") + + injected_text = "\n\n".join(injected) if injected else "No content injected" + return (enhanced, injected_text) class FunPackStoryMemJSONConverter: """ @@ -446,7 +481,6 @@ class FunPackImg2LatentInterpolation: return (output, preview) -# The standalone FunPackPromptEnhancer remains completely unchanged as requested class FunPackPromptEnhancer: @classmethod def INPUT_TYPES(s): @@ -1145,3 +1179,4 @@ NODE_DISPLAY_NAME_MAPPINGS = { + From ef0bec48c206b7e12dc265843666e349648ce6cc Mon Sep 17 00:00:00 2001 From: aimfordeb <133010426+aimfordeb@users.noreply.github.com> Date: Fri, 2 Jan 2026 06:40:30 +0200 Subject: [PATCH 7/9] Update funpack.py --- funpack.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/funpack.py b/funpack.py index 49598988..3026d47d 100644 --- a/funpack.py +++ b/funpack.py @@ -104,6 +104,26 @@ class FunPackLorebookEnhancer: return not all(matches) return True + def _get_effective_position(self, pos): + """Convert SillyTavern numeric position codes or strings to a safe insertion strategy.""" + if isinstance(pos, int): + # Common SillyTavern numeric positions + if pos == 0: + return "before_char" + elif pos in (1, 3): # 1=after char, 3=after examples / in-chat (most used for constants) + return "after_char" + elif pos == 2: + return "before_examples" + else: + print(f"[Lorebook Enhancer] Warning: Unknown numeric position {pos} → defaulting to after_char") + return "after_char" + + if isinstance(pos, str): + return pos.lower() + + print(f"[Lorebook Enhancer] Warning: Invalid position type {type(pos)} → defaulting to after_char") + return "after_char" + def _process_lorebook(self, path, scan_text, activated): if not path or not os.path.exists(path): print(f"[Lorebook Enhancer] File not found: {path}") @@ -113,7 +133,6 @@ class FunPackLorebookEnhancer: with open(path, 'r', encoding='utf-8') as f: lorebook = json.load(f) - # Support both list and dict format for entries entries = lorebook.get("entries", []) if isinstance(entries, dict): entries = list(entries.values()) @@ -151,7 +170,6 @@ class FunPackLorebookEnhancer: lorebook_1="", lorebook_2="", lorebook_3="", lorebook_4="", context_history="", scan_depth=4): - # Prepare scan text full_text = (context_history + "\n" + prompt).lower() lines = full_text.splitlines() scan_text = "\n".join(lines[-scan_depth:]) if scan_depth > 0 else full_text @@ -174,8 +192,10 @@ class FunPackLorebookEnhancer: if not content: continue - position = entry.get("position", "after_char").lower() - if position in ["after_char", "append", "bottom", "3"]: + # Safely handle position (now supports int <-> string mapping) + position = self._get_effective_position(entry.get("position", "after_char")) + + if position in ["after_char", "append", "bottom"]: enhanced += "\n" + content else: enhanced = content + "\n" + enhanced @@ -1180,3 +1200,4 @@ NODE_DISPLAY_NAME_MAPPINGS = { + From 340b663a1191ddd6c7809de138025ffb09f1027f Mon Sep 17 00:00:00 2001 From: aimfordeb <133010426+aimfordeb@users.noreply.github.com> Date: Fri, 2 Jan 2026 06:46:46 +0200 Subject: [PATCH 8/9] Update funpack.py --- funpack.py | 48 ++++++++++++------------------------------------ 1 file changed, 12 insertions(+), 36 deletions(-) diff --git a/funpack.py b/funpack.py index 3026d47d..6ee7cb00 100644 --- a/funpack.py +++ b/funpack.py @@ -33,8 +33,9 @@ HPSV3_QUALITY_THRESHOLD = 3.0 class FunPackLorebookEnhancer: """ - Injects context from SillyTavern-style lorebook JSON files based on keywords in the prompt. - Supports multiple lorebooks, constant entries, selective filtering, probability, etc. + Injects context from SillyTavern-style lorebook JSON files. + Always appends activated entries to the END of the prompt. + Supports multiple lorebooks, constants, selective filtering, probability, etc. """ @classmethod def INPUT_TYPES(cls): @@ -94,36 +95,12 @@ class FunPackLorebookEnhancer: secs = [s.strip() for s in secs.split(",") if s.strip()] matches = [self._match_keys([s], text) for s in secs] - if logic == 0: # ANY - return any(matches) - elif logic == 1: # ALL - return all(matches) - elif logic == 2: # NOT ANY - return not any(matches) - elif logic == 3: # NOT ALL - return not all(matches) + if logic == 0: return any(matches) + elif logic == 1: return all(matches) + elif logic == 2: return not any(matches) + elif logic == 3: return not all(matches) return True - def _get_effective_position(self, pos): - """Convert SillyTavern numeric position codes or strings to a safe insertion strategy.""" - if isinstance(pos, int): - # Common SillyTavern numeric positions - if pos == 0: - return "before_char" - elif pos in (1, 3): # 1=after char, 3=after examples / in-chat (most used for constants) - return "after_char" - elif pos == 2: - return "before_examples" - else: - print(f"[Lorebook Enhancer] Warning: Unknown numeric position {pos} → defaulting to after_char") - return "after_char" - - if isinstance(pos, str): - return pos.lower() - - print(f"[Lorebook Enhancer] Warning: Invalid position type {type(pos)} → defaulting to after_char") - return "after_char" - def _process_lorebook(self, path, scan_text, activated): if not path or not os.path.exists(path): print(f"[Lorebook Enhancer] File not found: {path}") @@ -185,20 +162,18 @@ class FunPackLorebookEnhancer: activated.sort(key=lambda e: e.get("insertion_order", e.get("order", 0))) injected = [] - enhanced = prompt + enhanced = prompt.strip() # clean up any trailing space for entry in activated: content = entry.get("content", "").strip() if not content: continue - # Safely handle position (now supports int <-> string mapping) - position = self._get_effective_position(entry.get("position", "after_char")) - - if position in ["after_char", "append", "bottom"]: + # Always append to the end - this is the requested behavior + if enhanced: enhanced += "\n" + content else: - enhanced = content + "\n" + enhanced + enhanced = content source = entry.get("comment") or entry.get("name") or f"uid:{entry.get('uid','?')}" or "unnamed" injected.append(f"[{source}] {content}") @@ -1200,4 +1175,5 @@ NODE_DISPLAY_NAME_MAPPINGS = { + From c3f90464a0aef83cb556cca81369aec795292111 Mon Sep 17 00:00:00 2001 From: aimfordeb <133010426+aimfordeb@users.noreply.github.com> Date: Mon, 5 Jan 2026 10:43:05 +0200 Subject: [PATCH 9/9] Update pyproject.toml --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fc20a5fe..d2816b03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ [project] name = "funpack" # Unique identifier for your node. Immutable after creation. description = "A set of nodes for experiments with video generation models, including Prompt Enhancer, Video Stitch, Creative Template and many other nodes." -version = "1.0.1" # Custom Node version. Must be semantically versioned. +version = "1.1.0" # Custom Node version. Must be semantically versioned. license = { text = "GNU GPL v3" } dependencies = [] # Filled in from requirements.txt @@ -11,5 +11,5 @@ Repository = "https://github.com/aimfordeb/ComfyUI-FunPack" [tool.comfy] PublisherId = "aimfordeb" # TODO (fill in Publisher ID from Comfy Registry Website). -DisplayName = "FunPack" # Display name for the Custom Node. Can be changed later. +DisplayName = "ComfyUI-FunPack" # Display name for the Custom Node. Can be changed later. Icon = "https://example.com/icon.png" # SVG, PNG, JPG or GIF (MAX. 800x400px)