feat: Enhance ▸ Reference takes a whole shortcuts file

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-09-24 00:58:28 +03:00
co-authored by Claude Opus 5.5
parent 7a7fb79d4a
commit dbe77d145f
3 changed files with 71 additions and 48 deletions
+53 -44
View File
@@ -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.
+4 -3
View File
@@ -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;
+14 -1
View File
@@ -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