From dbe77d145f50fe591bd7f375ce45c1fe8658787c Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Thu, 24 Sep 2026 00:58:28 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20Enhance=20=E2=96=B8=20Reference=20takes?= =?UTF-8?q?=20a=20whole=20shortcuts=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- conditioning.py | 97 +++++++++++++++++-------------- movie_editor/frontend/composer.js | 7 ++- tests/test_prompt_enhancer.py | 15 ++++- 3 files changed, 71 insertions(+), 48 deletions(-) diff --git a/conditioning.py b/conditioning.py index 1f44f042..e6280754 100644 --- a/conditioning.py +++ b/conditioning.py @@ -8968,67 +8968,76 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner): return "", f"{label}: generation failed: {error}" @staticmethod - def _v2_enhancer_sources(shortcut_keys=None, lorebook_paths=None): - """Load what Composer ▸ Enhance picked, once per run: (shortcuts, [(path, entries)]). - A missing shortcut or unreadable file is skipped and said so.""" - shortcuts = [] + def _v2_enhancer_sources(shortcut_keys=None, reference_paths=None): + """Load what Composer ▸ Enhance picked, once per run, as groups of entries — the + picked shortcuts as one group, then one group per file. A file is a SillyTavern + lorebook (`entries`) or a FunPack shortcuts file (`shortcuts`: the library itself or + an exported pack). Entry: {block, words, keys, constant}. Missing shortcuts and + unreadable files are skipped and said so.""" + try: + from .templates import load_shortcut_db, normalize_shortcut_db + except ImportError: + from templates import load_shortcut_db, normalize_shortcut_db + + def sc_entry(sc): + reps = [str(r) for r in (sc.get("replacements") or []) if str(r).strip()] + lines = [f"[Shortcut] {sc.get('name', '')}"] + lines += reps if len(reps) == 1 else [f"Variant {i + 1}: {r}" for i, r in enumerate(reps)] + # triggers are already expanded away, so the content counts as a mention too + return {"block": "\n".join(lines), + "words": [sc.get("name", "")] + list(sc.get("triggers") or []) + reps} + + groups = [] keys = [str(k) for k in (shortcut_keys or []) if str(k).strip()] if keys: - try: - from .templates import load_shortcut_db - except ImportError: - from templates import load_shortcut_db db = (load_shortcut_db() or {}).get("shortcuts", {}) - shortcuts = [db[k] for k in keys if isinstance(db.get(k), dict)] - books = [] - for path in (str(p).strip() for p in (lorebook_paths or [])): + groups.append([sc_entry(db[k]) for k in keys if isinstance(db.get(k), dict)]) + for path in (str(p).strip() for p in (reference_paths or [])): if not path: continue try: with open(path, "r", encoding="utf-8") as fh: - entries = json.load(fh).get("entries", []) - except (OSError, ValueError, AttributeError) as e: - print(f"[FunPackVideoRefinerV2] Prompt enhancer: lorebook {path} skipped — {e}") + data = json.load(fh) + if not isinstance(data, dict): + raise ValueError("not a lorebook or shortcuts file") + except (OSError, ValueError) as e: + print(f"[FunPackVideoRefinerV2] Prompt enhancer: reference {path} skipped — {e}") continue + if "shortcuts" in data: + shortcuts = normalize_shortcut_db(data).get("shortcuts", {}).values() + groups.append([sc_entry(sc) for sc in shortcuts if sc.get("enabled", True)]) + continue + entries = data.get("entries", []) if isinstance(entries, dict): entries = list(entries.values()) entries = sorted((e for e in entries if isinstance(e, dict) and str(e.get("content") or "").strip()), key=lambda e: e.get("insertion_order", e.get("order", 0))) - books.append((path, entries)) - return shortcuts, books + groups.append([{"block": f"[Lore] {e.get('comment') or e.get('name') or 'entry'}\n" + f"{str(e['content']).strip()}", + "keys": e.get("keys", e.get("key", [])), + "constant": bool(e.get("constant"))} for e in entries]) + return [g for g in groups if g] @staticmethod def _v2_enhancer_reference(text, sources): - """Reference appended after the prompt the enhancer rewrites. Per source — the picked - shortcuts as one, each lorebook file on its own — the entries the prompt mentions, or - the whole source when it mentions none (the model may need what the prompt does not - name). A shortcut is mentioned by its name, a trigger, or its content (already - expanded into the text); a lore entry by its keywords, constant entries joining any match.""" - shortcuts, books = sources or ([], []) + """Reference appended after the prompt the enhancer rewrites. Per group: the entries + the prompt mentions, or the whole group when it mentions none (the model may need + what the prompt does not name). Shortcuts are mentioned by name, trigger or content; + lore entries by their keywords, constant entries joining any match.""" low = str(text or "").lower() - blocks = [] - - def said(words): - return any(str(w).strip() and str(w).strip().lower() in low for w in words) - - def sc_block(sc): - reps = [r for r in (sc.get("replacements") or []) if str(r).strip()] - lines = [f"[Shortcut] {sc.get('name', '')}"] - lines += reps if len(reps) == 1 else [f"Variant {i + 1}: {r}" for i, r in enumerate(reps)] - return "\n".join(lines) - - hit = [sc for sc in shortcuts - if said([sc.get("name", "")] + list(sc.get("triggers") or []) + list(sc.get("replacements") or []))] - blocks += [sc_block(sc) for sc in (hit or shortcuts)] - lore = FunPackLorebookEnhancer() - for _path, entries in books: - # constants ride along with a match, but never count as one on their own - hit = [e for e in entries if lore._match_keys(e.get("keys", e.get("key", [])), low)] + + def mentioned(e): + if "keys" in e: + return lore._match_keys(e["keys"], low) + return any(str(w).strip() and str(w).strip().lower() in low for w in e["words"]) + + blocks = [] + for group in sources or []: + hit = [e for e in group if mentioned(e)] if hit: - hit = [e for e in entries if e.get("constant") or e in hit] - blocks += [f"[Lore] {e.get('comment') or e.get('name') or 'entry'}\n{str(e['content']).strip()}" - for e in (hit or entries)] + hit = [e for e in group if e.get("constant") or e in hit] + blocks += [e["block"] for e in (hit or group)] if not blocks: return "" return ("\n\nReference entries. Use an entry only where the prompt calls for it, " @@ -10640,7 +10649,7 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner): # Reference the user picked in Composer ▸ Enhance (shortcuts, lorebooks): loaded once # per run, matched against each prompt, appended after it. _enhance_sources = self._v2_enhancer_sources( - prompt_enhance_shortcuts, prompt_enhance_lorebooks) if prompt_enhance else ([], []) + prompt_enhance_shortcuts, prompt_enhance_lorebooks) if prompt_enhance else [] # When the run splits into scenes, the per-scene conditionings REPLACE this base entry # — so enhancing the base too would be a second full generation whose result is then # thrown away. One generation per prompt that is actually encoded, never two. diff --git a/movie_editor/frontend/composer.js b/movie_editor/frontend/composer.js index 38119da1..5b4b6d81 100644 --- a/movie_editor/frontend/composer.js +++ b/movie_editor/frontend/composer.js @@ -1071,7 +1071,7 @@ const box = el("div", "enh-ref"); box.append(el("div", "sw-rows-label", "Reference")); box.append(el("div", "insp-hint", - "Added after your prompt for the model to draw on, as name + content. What your prompt mentions is sent; when it mentions nothing from a lorebook (or from these shortcuts), all of it is.")); + "Added after your prompt for the model to draw on, as name + content. Pick shortcuts one by one, or add a whole file: a lorebook or a shortcuts file. What your prompt mentions is sent; when it mentions nothing from a file (or from the picked shortcuts), all of it is.")); const keys = (val("prompt_enhance_shortcuts") || []).map(String); const byKey = new Map((st.shortcuts || []).map((sc) => [String(sc.key), sc])); @@ -1107,8 +1107,9 @@ }); box.append(lbList); const lbRow = el("div", "enh-ref-add"); - const lbIn = el("input", "lib-in"); lbIn.placeholder = "Lorebook JSON path on the ComfyUI machine"; - const lbAdd = el("button", "btn ghost tiny", "+ Lorebook"); lbAdd.type = "button"; + const lbIn = el("input", "lib-in"); lbIn.placeholder = "Lorebook or shortcuts JSON path on the ComfyUI machine"; + lbIn.title = "A SillyTavern lorebook, or a FunPack shortcuts file — your whole library is user/default/FunPack/shortcuts.json"; + const lbAdd = el("button", "btn ghost tiny", "+ File"); lbAdd.type = "button"; const addLb = () => { const p = lbIn.value.trim(); if (!p || paths.includes(p)) return; diff --git a/tests/test_prompt_enhancer.py b/tests/test_prompt_enhancer.py index 7d40f758..05480f24 100644 --- a/tests/test_prompt_enhancer.py +++ b/tests/test_prompt_enhancer.py @@ -510,4 +510,17 @@ def test_reference_goes_after_the_prompt_and_not_into_the_readout(studio): def test_reference_is_empty_when_nothing_is_picked(): - assert C.FunPackVideoRefinerV2._v2_enhancer_reference("a cat", ([], [])) == "" + assert C.FunPackVideoRefinerV2._v2_enhancer_reference("a cat", []) == "" + + +def test_a_shortcuts_file_is_a_source_of_its_own(tmp_path, monkeypatch): + import json as _json + import templates + pack = tmp_path / "pack.json" + pack.write_text(_json.dumps({"shortcuts": { + "a": {"name": "Neon", "triggers": ["/neon"], "replacements": ["pink neon glow"]}, + "b": {"name": "Fog", "triggers": ["/fog"], "replacements": ["low fog"], "enabled": False}}})) + monkeypatch.setattr(templates, "load_shortcut_db", lambda: {"shortcuts": {}}) + src = C.FunPackVideoRefinerV2._v2_enhancer_sources([], [str(pack)]) + ref = C.FunPackVideoRefinerV2._v2_enhancer_reference("a street", src) + assert "[Shortcut] Neon\npink neon glow" in ref and "low fog" not in ref # disabled stays out