From 41bb188c18ee7805adb9e73a59b643e5b13d6db6 Mon Sep 17 00:00:00 2001 From: GenorTG Date: Tue, 10 Nov 2026 14:43:24 +0100 Subject: [PATCH] Add saved prompt library node --- h3_ideas.py | 38 +++++++++++++++++++++++++++++++++++++- moss_nodes.py | 16 ++++++++++++++++ pyproject.toml | 2 +- web/promptforge_h3.js | 41 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 2 deletions(-) diff --git a/h3_ideas.py b/h3_ideas.py index d5e5dfb..e7e17f7 100644 --- a/h3_ideas.py +++ b/h3_ideas.py @@ -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}) diff --git a/moss_nodes.py b/moss_nodes.py index 5032fbf..8bae4fa 100644 --- a/moss_nodes.py +++ b/moss_nodes.py @@ -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", diff --git a/pyproject.toml b/pyproject.toml index fed45c9..395683c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/web/promptforge_h3.js b/web/promptforge_h3.js index d68a425..3e58d8b 100644 --- a/web/promptforge_h3.js +++ b/web/promptforge_h3.js @@ -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 () {