Add saved prompt library node

This commit is contained in:
GenorTG
2026-11-10 14:43:24 +01:00
parent 38a01520c5
commit 41bb188c18
4 changed files with 95 additions and 2 deletions
+37 -1
View File
@@ -55,8 +55,44 @@ async def _delete(request):
return web.json_response(ideas)
def _library_path():
return image_library.store_dir() / "prompt_library.json"
def _library_read():
try:
return json.loads(_library_path().read_text(encoding="utf-8"))
except (OSError, ValueError):
return {}
@PromptServer.instance.routes.get("/promptforge/prompts")
async def _prompts(request):
return web.json_response(_library_read())
@PromptServer.instance.routes.post("/promptforge/prompts")
async def _prompts_save(request):
body = await request.json()
name = str(body.get("name", "")).strip()
if not name:
return web.json_response({"error": "a prompt needs a name"}, status=400)
data = _library_read()
data[name] = str(body.get("text", ""))
_library_path().write_text(json.dumps(data, indent=1, ensure_ascii=False), encoding="utf-8")
return web.json_response(data)
@PromptServer.instance.routes.delete("/promptforge/prompts")
async def _prompts_delete(request):
data = _library_read()
data.pop(request.query.get("name", ""), None)
_library_path().write_text(json.dumps(data, indent=1, ensure_ascii=False), encoding="utf-8")
return web.json_response(data)
@PromptServer.instance.routes.get("/promptforge/actions")
async def _actions(request):
return web.json_response({label: {"roles": h3_actions.role_info(label), "aspect": h3_actions.aspect(label),
"category": h3_actions.action_category(label), "cameras": h3_actions.cameras(label)}
"category": h3_actions.action_category(label), "cameras": h3_actions.cameras(label), "penetration": h3_actions.penetration_modes(label)}
for label in h3_actions.ACTIONS})
+16
View File
@@ -95,6 +95,20 @@ class PROMPTFORGE_KreaEditEncode:
return positive, negative_out
class PROMPTFORGE_PromptLibrary:
"""A saved prompt you can switch quickly; the bar comes from the browser side."""
CATEGORY = "🔮 PromptForge/Prompts"
RETURN_TYPES = ("STRING",)
FUNCTION = "get"
@classmethod
def INPUT_TYPES(cls):
return {"required": {"text": ("STRING", {"default": "", "multiline": True, "tooltip": "Use the bar to load, save or delete prompts."})}}
def get(self, text):
return (text,)
EDIT_LORAS = {
"Krea 2": "Krea 2/tool/krea2_identity_edit_v2.safetensors",
"MOSS Image": "MOSS Image/tool/moss_image_edit_lora_rank_64_bf16.safetensors",
@@ -270,6 +284,7 @@ NODE_CLASS_MAPPINGS = {
"PROMPTFORGE_TextEncode": PROMPTFORGE_TextEncode,
"PROMPTFORGE_MossEditEncode": PROMPTFORGE_MossEditEncode,
"PROMPTFORGE_KreaEditEncode": PROMPTFORGE_KreaEditEncode,
"PROMPTFORGE_PromptLibrary": PROMPTFORGE_PromptLibrary,
"PROMPTFORGE_EditLora": PROMPTFORGE_EditLora,
"PROMPTFORGE_KreaNAG": PROMPTFORGE_KreaNAG,
"PROMPTFORGE_KreaEditModelPatch": PROMPTFORGE_KreaEditModelPatch,
@@ -282,6 +297,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"PROMPTFORGE_TextEncode": "PromptForge Text Encode",
"PROMPTFORGE_MossEditEncode": "PromptForge MOSS Edit Encode",
"PROMPTFORGE_KreaEditEncode": "PromptForge Krea Edit Encode",
"PROMPTFORGE_PromptLibrary": "PromptForge Prompt Library",
"PROMPTFORGE_EditLora": "PromptForge Edit LoRA",
"PROMPTFORGE_KreaNAG": "PromptForge Krea NAG",
"PROMPTFORGE_KreaEditModelPatch": "PromptForge Krea Edit Model Patch",
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "ComfyUI-PromptForge"
description = "ComfyUI prompt enhancement suite: model/config node with GPU-aware auto settings, prompt enhancer with a preset catalog, and a standalone config finder."
version = "1.28.30"
version = "1.28.31"
license = { file = "LICENSE" }
dependencies = [
"transformers",
+41
View File
@@ -10,6 +10,7 @@ const MODES = "PROMPTFORGE_H3Modes";
const SECOND_STAGE = "PROMPTFORGE_H3SecondStage";
const GENERATE = "PROMPTFORGE_H3Generate";
const REFERENCES = "PROMPTFORGE_H3References";
const LIBRARY = "PROMPTFORGE_PromptLibrary";
const SUBJECT_DISABLED = "disabled (not in the video)";
const ANY_CAMERA = "(writer's choice)";
// the description box of a slot is shown only while its picture / video / sound is connected and the description is not itself wired
@@ -456,6 +457,46 @@ app.registerExtension({
return;
}
if (nodeData.name === LIBRARY) {
const onCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onCreated?.apply(this, arguments);
const node = this;
const text = node.widgets.find((widget) => widget.name === "text");
const bar = el("div", { display: "flex", alignItems: "center", gap: "4px", fontSize: "12px", color: "var(--input-text, #ddd)" });
const select = el("select", { ...INPUT, flex: "1 1 auto", minWidth: "0", height: "24px" });
const previous = el("button", BUTTON, { textContent: "◀" });
const next = el("button", BUTTON, { textContent: "▶" });
const save = el("button", BUTTON, { textContent: "Save" });
const saveAs = el("button", BUTTON, { textContent: "Save as…" });
const remove = el("button", BUTTON, { textContent: "Delete" });
bar.append(el("span", { opacity: ".7" }, { textContent: "Prompts" }), previous, select, next, save, saveAs, remove);
let prompts = {};
const refresh = (chosen) => {
select.replaceChildren(el("option", {}, { value: "", textContent: "— saved prompts —" }), ...Object.keys(prompts).map((name) => el("option", {}, { value: name, textContent: name })));
select.value = chosen && prompts[chosen] !== undefined ? chosen : "";
};
const load = async () => { prompts = await (await api.fetchApi("/promptforge/prompts")).json(); refresh(); };
const store = async (name) => { prompts = await (await api.fetchApi("/promptforge/prompts", { method: "POST", body: JSON.stringify({ name, text: text.value }) })).json(); refresh(name); };
select.addEventListener("change", () => { if (prompts[select.value] !== undefined) { text.value = prompts[select.value]; node.setDirtyCanvas(true, true); } });
previous.addEventListener("click", () => { const names = Object.keys(prompts); if (!names.length) return; const at = (names.indexOf(select.value) - 1 + names.length) % names.length; refresh(names[at]); text.value = prompts[names[at]]; node.setDirtyCanvas(true, true); });
next.addEventListener("click", () => { const names = Object.keys(prompts); if (!names.length) return; const at = (names.indexOf(select.value) + 1) % names.length; refresh(names[at]); text.value = prompts[names[at]]; node.setDirtyCanvas(true, true); });
save.addEventListener("click", () => { const name = select.value || window.prompt("Name of this prompt:"); if (name?.trim()) store(name.trim()); });
saveAs.addEventListener("click", () => { const name = window.prompt("Name of this prompt:", select.value); if (name?.trim()) store(name.trim()); });
remove.addEventListener("click", async () => {
if (!select.value || !window.confirm(`Delete the prompt "${select.value}"?`)) return;
prompts = await (await api.fetchApi("/promptforge/prompts?name=" + encodeURIComponent(select.value), { method: "DELETE" })).json();
refresh("");
});
const widget = domWidget(node, "saved_prompts", bar, () => 30);
node.widgets.splice(node.widgets.indexOf(widget), 1);
node.widgets.splice(node.widgets.indexOf(text), 0, widget);
load();
node.setSize([Math.max(node.size[0], 520), Math.max(node.size[1], node.computeSize()[1])]);
};
return;
}
if (nodeData.name !== PARTS) return;
const onCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {