From 121ff083c6ac19278450350009b80a107646c18d Mon Sep 17 00:00:00 2001 From: Raykosan <71210053+Raykosan@users.noreply.github.com> Date: Wed, 29 Apr 2026 03:25:30 +0300 Subject: [PATCH] Modification of the RS Prompts node to work with LLM --- Rayko_Prompts.py | 155 ++++++++++++++++--- __init__.py | 2 +- package.json | 2 +- prompts/1.json | 3 + prompts/2.json | 3 + prompts/3.json | 3 + pyproject.toml | 2 +- web/rs_prompts.js | 368 +++++++++++++++++++++++++++++++++++++++++----- 8 files changed, 476 insertions(+), 62 deletions(-) create mode 100644 prompts/1.json create mode 100644 prompts/2.json create mode 100644 prompts/3.json diff --git a/Rayko_Prompts.py b/Rayko_Prompts.py index f828a6f..fe7e0be 100644 --- a/Rayko_Prompts.py +++ b/Rayko_Prompts.py @@ -3,6 +3,8 @@ import os import server import torch from aiohttp import web +from concurrent.futures import Future +import time CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) PROMPTS_DIR = os.path.join(CURRENT_DIR, "prompts") @@ -10,14 +12,22 @@ PROMPTS_DIR = os.path.join(CURRENT_DIR, "prompts") if not os.path.exists(PROMPTS_DIR): os.makedirs(PROMPTS_DIR) +PENDING_PROMPTS = {} + class RSPrompts: @classmethod def INPUT_TYPES(cls): return { "required": { - "clip": ("CLIP", {"tooltip": "CLIP model to encode prompts with."}), - "Positive prompt": ("STRING", {"default": "", "multiline": True}), - "Negative prompt": ("STRING", {"default": "", "multiline": True}), + "clip": ("CLIP",), + "text": ("STRING", {"multiline": True, "default": "", "hidden": True}), + }, + "optional": { + "text_input": ("STRING", {"forceInput": True}), + "pause_for_edit": ("BOOLEAN", {"default": False, "label": "⏸️ Pause for manual edit"}), + }, + "hidden": { + "unique_id": "UNIQUE_ID", } } @@ -25,37 +35,79 @@ class RSPrompts: RETURN_NAMES = ("POSITIVE", "NEGATIVE") FUNCTION = "encode_prompts" CATEGORY = "🦊 RaykoStudio" - DESCRIPTION = "Combined positive and negative prompt encoder with preset management." + DESCRIPTION = "Text encoder with visual prompt controls and pause-for-edit mode." - def encode_prompts(self, clip, **kwargs): - positive = kwargs.get("Positive prompt", "") - negative = kwargs.get("Negative prompt", "") - - tokens_pos = clip.tokenize(positive) + def encode_prompts(self, clip, pause_for_edit=False, text="", text_input=None, unique_id=None): + current_text = text_input if text_input is not None else text + + if pause_for_edit and text_input is not None and current_text and unique_id: + print(f"[RS Prompts 🦊] Pause mode enabled for node {unique_id}") + + PENDING_PROMPTS[unique_id] = { + "original": current_text, + "edited": current_text, + "status": "pending", + "timestamp": time.time(), + "clip": clip + } + + from server import PromptServer + PromptServer.instance.send_sync("rs.prompt.pause", { + "node_id": unique_id, + "prompt": current_text + }) + + future = Future() + PENDING_PROMPTS[unique_id]["future"] = future + + try: + result = future.result(timeout=600) + if result["status"] == "approved": + final_text = result["prompt"] + print(f"[RS Prompts 🦊] Using edited prompt: {final_text[:50]}...") + else: + final_text = current_text + print(f"[RS Prompts 🦊] Using original prompt") + except TimeoutError: + print(f"[RS Prompts 🦊] Timeout! Using original prompt") + final_text = current_text + except Exception as e: + print(f"[RS Prompts 🦊] Error: {e}, using original") + final_text = current_text + finally: + if unique_id in PENDING_PROMPTS: + del PENDING_PROMPTS[unique_id] + + current_text = final_text + + tokens_pos = clip.tokenize(current_text) pos_cond = clip.encode_from_tokens_scheduled(tokens_pos) - tokens_neg = clip.tokenize(negative) + tokens_neg = clip.tokenize("") neg_cond = clip.encode_from_tokens_scheduled(tokens_neg) - return (pos_cond, neg_cond) + return { + "ui": {"text": [current_text]}, + "result": (pos_cond, neg_cond) + } + + @classmethod + def IS_CHANGED(cls, **kwargs): + return float("nan") -NODE_CLASS_MAPPINGS = {"RSPrompts": RSPrompts} -NODE_DISPLAY_NAME_MAPPINGS = {"RSPrompts": "🦊 RS Prompts"} @server.PromptServer.instance.routes.post("/rs_prompts/save_prompt") async def rs_prompts_save_prompt(request): try: data = await request.json() name = data.get("name", "").strip() - if not name: return web.Response(status=400, text="Name required") + if not name: + return web.Response(status=400, text="Name required") name = "".join(c for c in name if c.isalnum() or c in " _-").strip() - if not name: return web.Response(status=400, text="Invalid name") - + if not name: + return web.Response(status=400, text="Invalid name") filepath = os.path.join(PROMPTS_DIR, f"{name}.json") - prompt_data = { - "positive": data.get("positive", ""), - "negative": data.get("negative", "") - } + prompt_data = {"text": data.get("text", "")} with open(filepath, 'w', encoding='utf-8') as f: json.dump(prompt_data, f, indent=2) return web.Response(status=200, text="OK") @@ -90,11 +142,70 @@ async def rs_prompts_delete_prompt(request): try: data = await request.json() name = data.get("name") - if not name: return web.Response(status=400, text="Name required") + if not name: + return web.Response(status=400, text="Name required") filepath = os.path.join(PROMPTS_DIR, f"{name}.json") if os.path.exists(filepath): os.remove(filepath) return web.Response(status=200, text="OK") return web.Response(status=404, text="Prompt not found") except Exception as e: - return web.Response(status=500, text=str(e)) \ No newline at end of file + return web.Response(status=500, text=str(e)) + + +@server.PromptServer.instance.routes.post("/rs_prompts/approve_edit") +async def rs_prompts_approve_edit(request): + try: + data = await request.json() + node_id = data.get("node_id") + edited_prompt = data.get("prompt", "") + + if node_id in PENDING_PROMPTS: + pending = PENDING_PROMPTS[node_id] + pending["status"] = "approved" + pending["edited"] = edited_prompt + + if "future" in pending: + pending["future"].set_result({ + "status": "approved", + "prompt": edited_prompt + }) + + print(f"[RS Prompts 🦊] Node {node_id} approved with new prompt") + return web.Response(status=200, text="Approved") + else: + return web.Response(status=404, text=f"Node {node_id} not waiting") + + except Exception as e: + print(f"[RS Prompts 🦊] Error in approve_edit: {e}") + return web.Response(status=500, text=str(e)) + + +@server.PromptServer.instance.routes.post("/rs_prompts/reject_edit") +async def rs_prompts_reject_edit(request): + try: + data = await request.json() + node_id = data.get("node_id") + + if node_id in PENDING_PROMPTS: + pending = PENDING_PROMPTS[node_id] + pending["status"] = "rejected" + + if "future" in pending: + pending["future"].set_result({ + "status": "rejected", + "prompt": pending["original"] + }) + + print(f"[RS Prompts 🦊] Node {node_id} rejected, using original") + return web.Response(status=200, text="Rejected") + else: + return web.Response(status=404, text=f"Node {node_id} not waiting") + + except Exception as e: + print(f"[RS Prompts 🦊] Error in reject_edit: {e}") + return web.Response(status=500, text=str(e)) + + +NODE_CLASS_MAPPINGS = {"RSPrompts": RSPrompts} +NODE_DISPLAY_NAME_MAPPINGS = {"RSPrompts": "🦊 RS Prompts"} \ No newline at end of file diff --git a/__init__.py b/__init__.py index f5acb72..c04297f 100644 --- a/__init__.py +++ b/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.17.0" +__version__ = "0.17.1" import importlib import logging from pathlib import Path diff --git a/package.json b/package.json index afd0b1b..2f5813d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ComfyUI_RaykoStudio", - "version": "0.17.0", + "version": "0.17.1", "description": "Set of custom nodes for ComfyUI providing additional image processing capabilities", "author": "Raykosan", "license": "MIT", diff --git a/prompts/1.json b/prompts/1.json new file mode 100644 index 0000000..2aa8c8c --- /dev/null +++ b/prompts/1.json @@ -0,0 +1,3 @@ +{ + "text": "Roshii art style, a rugged, middle-aged man with a thick beard and spiky short hair stands confidently on a gritty urban street at dusk; he wears dark sunglasses, smokes a cigar emitting smoke into the air, and has tattoos covering his left arm while wearing a torn leather jacket over a white t-shirt revealing part of his chest; his right hand grips a silver pistol holstered at hip level, adorned with various pouches and gear strapped to his waistband; background features towering buildings under a hazy teal sky with neon signs glowing faintly \u00e2 one red sign displays Chinese characters \u00e2 evoking cyberpunk meets post-apocalyptic aesthetics; detailed brushwork mimics ink wash textures with sharp contrasts between shadowed forms and illuminated surfaces; intense gaze directed off-camera as if surveying danger ahead; overall composition emphasizes raw masculinity, solitude, and edgy survivalist vibe through dynamic lighting and atmospheric depth." +} \ No newline at end of file diff --git a/prompts/2.json b/prompts/2.json new file mode 100644 index 0000000..85c90c8 --- /dev/null +++ b/prompts/2.json @@ -0,0 +1,3 @@ +{ + "text": "A digital illustration shoot from the side about a muscular young man standing confidently in a dimly lit library, holding a knife in his right hand. the image also shows the man, who appears to be in his mid-twenties, standing in the middle of the image, with a serious expression on his face. he has short, black hair and is wearing a black jacket over a white shirt, paired with dark jeans. the man is holding a knife in his left hand, and his right hand is clenched into fists. he is standing in a dimly lit library, with tall wooden shelves filled with books, and the lighting is dim, creating a moody atmosphere. the image has a realistic style with detailed shading and highlights, giving it a polished look. the man is positioned in the center of the image, with his full body visible, and his posture conveys a sense of strength and determination. he is wearing a black jacket over a white shirt, and his black jeans are tucked into his pants, completing the look." +} \ No newline at end of file diff --git a/prompts/3.json b/prompts/3.json new file mode 100644 index 0000000..548c187 --- /dev/null +++ b/prompts/3.json @@ -0,0 +1,3 @@ +{ + "text": "A photo-realistic portrait shoot from a portrait angle about a young woman with wavy brown hair and freckles, wearing a blue and gold embroidered vest and white blouse. in the middle of the image, a young woman appears to be in her early twenties, with fair skin, brown hair, and green eyes, looking directly at the viewer with a serious expression. she is wearing a blue vest with intricate gold embroidery and white blouse with long sleeves, and a brown belt with a gold buckle. her hair is blowing in the wind, and she is wearing dangling earrings. the background is blurred, with a soft, muted color palette. the lighting is soft and natural, highlighting her delicate features and the intricate details of the vest and blouse. the image has a serene and contemplative atmosphere, with the woman's expression conveying a sense of calmness and introspection." +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6127ca2..bcd9bf6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ComfyUI_RaykoStudio" description = "Set of custom nodes for ComfyUI providing additional image processing capabilities" -version = "0.17.0" +version = "0.17.1" license = { file = "LICENSE" } dependencies = ["torch>=1.7.0", "numpy>=1.19.0", "Pillow>=8.0.0", "freetype-py>=2.5.1"] diff --git a/web/rs_prompts.js b/web/rs_prompts.js index 1016463..6444cd2 100644 --- a/web/rs_prompts.js +++ b/web/rs_prompts.js @@ -4,7 +4,88 @@ import { api } from "../../scripts/api.js"; const NODE_CLASS = "RSPrompts"; let pendingDeleteName = null; -function mkEl(tag, css) { const el = document.createElement(tag); if (css) el.style.cssText = css; return el; } +const styleBlock = document.createElement('style'); +styleBlock.innerHTML = ` + textarea.comfy-multiline-input { opacity: 1 !important; color: #fff !important; text-shadow: none !important; } + textarea.comfy-multiline-input:disabled { opacity: 0.5 !important; color: #888 !important; } + .rs-waiting-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0,0,0,0.85); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + border-radius: 8px; + } + .rs-waiting-message { + background: #2a2a2a; + padding: 20px; + border-radius: 8px; + border: 1px solid #5090cc; + text-align: center; + } + /* Toggle switch styles */ + .rs-toggle-switch { + position: relative; + display: inline-flex; + align-items: center; + cursor: pointer; + gap: 8px; + } + .rs-toggle-switch input { + opacity: 0; + width: 0; + height: 0; + position: absolute; + } + .rs-toggle-slider { + position: relative; + display: inline-block; + width: 40px; + height: 20px; + background-color: #444; + border-radius: 20px; + transition: 0.3s; + cursor: pointer; + } + .rs-toggle-slider:before { + position: absolute; + content: ""; + height: 16px; + width: 16px; + left: 2px; + bottom: 2px; + background-color: white; + border-radius: 50%; + transition: 0.3s; + } + input:checked + .rs-toggle-slider { + background-color: #28a745; + } + input:checked + .rs-toggle-slider:before { + transform: translateX(20px); + } + .rs-toggle-label { + font-size: 11px; + color: #ccc; + user-select: none; + } + .rs-toggle-active .rs-toggle-label { + color: #28a745; + font-weight: bold; + } +`; +document.head.appendChild(styleBlock); + +function mkEl(tag, css) { + const el = document.createElement(tag); + if (css) el.style.cssText = css; + return el; +} app.registerExtension({ name: "RSPrompts", @@ -16,43 +97,147 @@ app.registerExtension({ nodeType.prototype.onNodeCreated = function () { const result = origOnNodeCreated?.apply(this, arguments); const node = this; + + const hidePhantomSlot = () => { + if (node.inputs) { + const textInput = node.inputs.find(i => i.name === "text"); + if (textInput) { + textInput.disabled = true; + textInput.color_on = "transparent"; + textInput.color_off = "transparent"; + textInput.pos = [-1000, -1000]; + } + } + }; + setTimeout(hidePhantomSlot, 0); + + const textWidget = node.widgets?.find(w => w.name === "text"); + const pauseWidget = node.widgets?.find(w => w.name === "pause_for_edit"); - const widgets = { - positive: node.widgets.find(w => w.name === "Positive prompt"), - negative: node.widgets.find(w => w.name === "Negative prompt") + if (pauseWidget) { + pauseWidget.hidden = true; + pauseWidget.serializeValue = () => toggleInput.checked; + } + + if (textWidget) textWidget.hidden = true; + + const enableWidgets = () => { + if (textWidget) { + textWidget.disabled = false; + textWidget.serializeValue = () => textWidget.value; + } + }; + setTimeout(enableWidgets, 50); + + const origOnConnectionsChange = node.onConnectionsChange; + node.onConnectionsChange = function(slotType, slotIndex, isConnected, link, linkInfo) { + if (origOnConnectionsChange) origOnConnectionsChange.apply(this, arguments); + setTimeout(enableWidgets, 50); }; - const root = mkEl("div", "display:flex;flex-direction:column;gap:2px;padding:0px 4px 4px 4px;width:100%;box-sizing:border-box;position:relative;margin-top:0px;"); + let waitingOverlay = null; + let pauseModeEnabled = false; + + const removeWaitingOverlay = () => { + if (waitingOverlay && waitingOverlay.parentNode) { + waitingOverlay.remove(); + waitingOverlay = null; + } + }; + + const showWaitingOverlay = () => { + removeWaitingOverlay(); + waitingOverlay = mkEl("div", "rs-waiting-overlay"); + const messageDiv = mkEl("div", "rs-waiting-message"); + messageDiv.innerHTML = ` +