Adding an RS Image-Prompt node
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright 2025-2026 Raykosan (RaykoStudio)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import json
|
||||
from PIL import Image
|
||||
import folder_paths
|
||||
from server import PromptServer
|
||||
from aiohttp import web
|
||||
|
||||
WEB_DIRECTORY = "./web"
|
||||
|
||||
KNOWN_PROMPT_NODES = {"CLIPTextEncode", "RSPrompts", "RS_ImagePrompt"}
|
||||
TEXT_KEYS = ["text", "string", "parameters", "Comment", "Description", "Title"]
|
||||
|
||||
def get_image_files():
|
||||
input_dir = folder_paths.get_input_directory()
|
||||
files = []
|
||||
for f in os.listdir(input_dir):
|
||||
if os.path.isfile(os.path.join(input_dir, f)) and f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp')):
|
||||
files.append(f)
|
||||
files.sort()
|
||||
return files
|
||||
|
||||
def _get_text_from_node(prompt_data, node_id, visited):
|
||||
node_id = str(node_id)
|
||||
if node_id in visited or node_id not in prompt_data:
|
||||
return ""
|
||||
visited.add(node_id)
|
||||
|
||||
node = prompt_data[node_id]
|
||||
inputs = node.get("inputs", {})
|
||||
class_type = node.get("class_type", "")
|
||||
|
||||
if inputs.get("enable_text_input"):
|
||||
ti = inputs.get("text_input")
|
||||
if isinstance(ti, list) and len(ti) >= 1:
|
||||
upstream = _get_text_from_node(prompt_data, ti[0], visited)
|
||||
if upstream:
|
||||
return upstream
|
||||
|
||||
text = inputs.get("text")
|
||||
if isinstance(text, str) and text:
|
||||
return text
|
||||
|
||||
if class_type == "RS_ImagePrompt":
|
||||
prompt_preview = inputs.get("prompt_preview")
|
||||
if isinstance(prompt_preview, str) and prompt_preview:
|
||||
return prompt_preview
|
||||
|
||||
for key in ("conditioning", "conditioning_1", "conditioning_2", "clip", "text_input", "text"):
|
||||
ref = inputs.get(key)
|
||||
if isinstance(ref, list) and len(ref) >= 1:
|
||||
result = _get_text_from_node(prompt_data, ref[0], visited)
|
||||
if result:
|
||||
return result
|
||||
return ""
|
||||
|
||||
def _extract_prompt_from_dict(prompt_data):
|
||||
if not isinstance(prompt_data, dict):
|
||||
return ""
|
||||
|
||||
negative_refs = set()
|
||||
for node_data in prompt_data.values():
|
||||
if not isinstance(node_data, dict):
|
||||
continue
|
||||
ref = node_data.get("inputs", {}).get("negative")
|
||||
if isinstance(ref, list) and len(ref) >= 1:
|
||||
negative_refs.add((str(ref[0]), ref[1] if len(ref) > 1 else 0))
|
||||
|
||||
for node_id, node_data in prompt_data.items():
|
||||
if not isinstance(node_data, dict):
|
||||
continue
|
||||
inputs = node_data.get("inputs", {})
|
||||
if "positive" in inputs:
|
||||
pos_ref = inputs.get("positive")
|
||||
if isinstance(pos_ref, list) and pos_ref:
|
||||
text = _get_text_from_node(prompt_data, pos_ref[0], set())
|
||||
if text:
|
||||
return text
|
||||
|
||||
for node_id, node_data in prompt_data.items():
|
||||
if not isinstance(node_data, dict):
|
||||
continue
|
||||
if node_data.get("class_type", "") in KNOWN_PROMPT_NODES:
|
||||
if (str(node_id), 0) in negative_refs:
|
||||
continue
|
||||
text = node_data.get("inputs", {}).get("text")
|
||||
if isinstance(text, str) and text:
|
||||
return text
|
||||
prompt_preview = node_data.get("inputs", {}).get("prompt_preview")
|
||||
if isinstance(prompt_preview, str) and prompt_preview:
|
||||
return prompt_preview
|
||||
|
||||
return ""
|
||||
|
||||
def _extract_prompt(prompt_json_str):
|
||||
try:
|
||||
data = json.loads(prompt_json_str)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return ""
|
||||
return _extract_prompt_from_dict(data)
|
||||
|
||||
def _extract_prompt_from_workflow(workflow_json_str):
|
||||
try:
|
||||
wf = json.loads(workflow_json_str)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return ""
|
||||
if not isinstance(wf, dict):
|
||||
return ""
|
||||
|
||||
nodes = wf.get("nodes", [])
|
||||
links = wf.get("links", [])
|
||||
if not isinstance(nodes, list):
|
||||
return ""
|
||||
|
||||
nodes_by_id = {}
|
||||
for n in nodes:
|
||||
if isinstance(n, dict) and "id" in n:
|
||||
nodes_by_id[str(n["id"])] = n
|
||||
|
||||
link_by_target = {}
|
||||
for lk in links:
|
||||
if isinstance(lk, list) and len(lk) >= 5:
|
||||
link_by_target[(str(lk[3]), lk[4])] = (str(lk[1]), lk[2])
|
||||
|
||||
def widget_text(node):
|
||||
wv = node.get("widgets_values", [])
|
||||
if isinstance(wv, list) and wv and isinstance(wv[0], str):
|
||||
return wv[0]
|
||||
return ""
|
||||
|
||||
def trace(node_id, slot_index, visited):
|
||||
key = (node_id, slot_index)
|
||||
if key in visited:
|
||||
return ""
|
||||
visited.add(key)
|
||||
src = link_by_target.get(key)
|
||||
if not src:
|
||||
return ""
|
||||
src_id, _ = src
|
||||
src_node = nodes_by_id.get(src_id)
|
||||
if not src_node:
|
||||
return ""
|
||||
if src_node.get("type", "") in KNOWN_PROMPT_NODES:
|
||||
t = widget_text(src_node)
|
||||
if t:
|
||||
return t
|
||||
for idx in range(len(src_node.get("inputs", []))):
|
||||
res = trace(src_id, idx, visited)
|
||||
if res:
|
||||
return res
|
||||
return ""
|
||||
|
||||
for nid, node in nodes_by_id.items():
|
||||
ntype = str(node.get("type", ""))
|
||||
if "ampler" in ntype:
|
||||
for idx, inp in enumerate(node.get("inputs", [])):
|
||||
if isinstance(inp, dict) and inp.get("name") == "positive":
|
||||
t = trace(nid, idx, set())
|
||||
if t:
|
||||
return t
|
||||
|
||||
for nid, node in nodes_by_id.items():
|
||||
if node.get("type", "") in KNOWN_PROMPT_NODES:
|
||||
t = widget_text(node)
|
||||
if t:
|
||||
return t
|
||||
return ""
|
||||
|
||||
def _looks_like_settings(line):
|
||||
keys = ("Steps:", "Sampler:", "CFG scale:", "Seed:", "Size:", "Model hash:",
|
||||
"Model:", "Denoising strength:", "Clip skip:", "ENSAM:", "TI_hashes:")
|
||||
return any(k in line for k in keys)
|
||||
|
||||
|
||||
def _clean_text_prompt(text):
|
||||
if not isinstance(text, str):
|
||||
return ""
|
||||
text = text.strip()
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
if text.startswith("{"):
|
||||
try:
|
||||
data = json.loads(text)
|
||||
res = _extract_prompt_from_dict(data)
|
||||
if res:
|
||||
return res
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
|
||||
idx = text.find("Negative prompt:")
|
||||
if idx != -1:
|
||||
text = text[:idx]
|
||||
lines = text.split("\n")
|
||||
while lines and _looks_like_settings(lines[-1]):
|
||||
lines.pop()
|
||||
return "\n".join(lines).strip()
|
||||
|
||||
def _decode_user_comment(data):
|
||||
if isinstance(data, str):
|
||||
return data
|
||||
if not isinstance(data, (bytes, bytearray)):
|
||||
return str(data)
|
||||
|
||||
if data.startswith(b"ASCII\x00\x00\x00"):
|
||||
return data[8:].decode("latin-1", errors="replace")
|
||||
if data.startswith(b"UNICODE\x00"):
|
||||
return data[8:].decode("utf-16", errors="replace")
|
||||
if data.startswith(b"JIS\x00\x00\x00\x00\x00"):
|
||||
return data[8:].decode("shift_jis", errors="replace")
|
||||
|
||||
for encoding in ("utf-8", "latin-1"):
|
||||
try:
|
||||
return data.decode(encoding)
|
||||
except (UnicodeDecodeError, UnicodeError):
|
||||
continue
|
||||
return data.decode("latin-1", errors="replace")
|
||||
|
||||
|
||||
def _extract_a1111_from_exif(pil_img):
|
||||
try:
|
||||
exif = pil_img.getexif()
|
||||
if not exif:
|
||||
return ""
|
||||
exif_ifd = exif.get_ifd(0x8769)
|
||||
if not exif_ifd:
|
||||
return ""
|
||||
user_comment = exif_ifd.get(0x9286)
|
||||
if not user_comment:
|
||||
return ""
|
||||
return _decode_user_comment(user_comment)
|
||||
except Exception as e:
|
||||
print(f"[RS Image-Prompt] EXIF error: {e}")
|
||||
return ""
|
||||
|
||||
def extract_prompt_from_file(input_path):
|
||||
try:
|
||||
with Image.open(input_path) as pil_img:
|
||||
info = dict(pil_img.info)
|
||||
exif_text = _extract_a1111_from_exif(pil_img)
|
||||
except Exception as e:
|
||||
print(f"[RS Image-Prompt] Cannot open image: {e}")
|
||||
return ""
|
||||
|
||||
prompt_json = info.get("prompt", "")
|
||||
if prompt_json:
|
||||
result = _extract_prompt(prompt_json)
|
||||
if result:
|
||||
return result
|
||||
|
||||
workflow_json = info.get("workflow", "")
|
||||
if workflow_json:
|
||||
result = _extract_prompt_from_workflow(workflow_json)
|
||||
if result:
|
||||
return result
|
||||
|
||||
for key in TEXT_KEYS:
|
||||
value = info.get(key)
|
||||
if isinstance(value, str) and value.strip():
|
||||
result = _clean_text_prompt(value)
|
||||
if result:
|
||||
return result
|
||||
|
||||
if exif_text:
|
||||
result = _clean_text_prompt(exif_text)
|
||||
if result:
|
||||
return result
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/rayko/get_prompt")
|
||||
async def rayko_get_prompt(request):
|
||||
filename = request.rel_url.query.get("filename", "")
|
||||
if not filename:
|
||||
return web.json_response({"prompt": ""})
|
||||
try:
|
||||
path = folder_paths.get_annotated_filepath(filename)
|
||||
return web.json_response({"prompt": extract_prompt_from_file(path)})
|
||||
except Exception as e:
|
||||
return web.json_response({"prompt": "", "error": str(e)})
|
||||
|
||||
|
||||
class RS_ImagePrompt:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
files = get_image_files()
|
||||
return {
|
||||
"required": {
|
||||
"image": ("STRING", {"default": ""}),
|
||||
"prompt_preview": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
"tooltip": "Extracted prompt preview (updates on file select)",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("prompt",)
|
||||
FUNCTION = "process"
|
||||
CATEGORY = "🦊 RaykoStudio"
|
||||
DESCRIPTION = "Extracts the positive prompt from image metadata (ComfyUI, RS Prompts, RS Image-Prompt, A1111 PNG/JPG/WebP)."
|
||||
|
||||
def process(self, image, prompt_preview=""):
|
||||
try:
|
||||
input_path = folder_paths.get_annotated_filepath(image)
|
||||
if not os.path.exists(input_path):
|
||||
raise FileNotFoundError(f"File not found: {input_path}")
|
||||
return (extract_prompt_from_file(input_path),)
|
||||
except Exception as e:
|
||||
print(f"[RS Image-Prompt] Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return ("",)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"RS_ImagePrompt": RS_ImagePrompt,
|
||||
}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"RS_ImagePrompt": "🦊 RS Image-Prompt",
|
||||
}
|
||||
+1
-1
@@ -29,7 +29,7 @@ IGNORED_MODULES: Set[str] = {'__init__.py'}
|
||||
REQUIRED_MODULES: Set[str] = set()
|
||||
|
||||
PACKAGE_DISPLAY_NAME = "ComfyUI_RaykoStudio"
|
||||
__version__ = "0.34.1"
|
||||
__version__ = "0.35.0"
|
||||
|
||||
def load_modules():
|
||||
print(f"🦊 {PACKAGE_DISPLAY_NAME}: Version: {__version__}")
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ComfyUI_RaykoStudio",
|
||||
"version": "0.34.1",
|
||||
"version": "0.35.0",
|
||||
"description": "Set of custom nodes for ComfyUI providing additional image processing capabilities",
|
||||
"author": "Raykosan",
|
||||
"license": "Apache License 2.0",
|
||||
@@ -17,6 +17,7 @@
|
||||
"Rayko_Color_Picker.py"
|
||||
"Rayko_Crop_Image.py"
|
||||
"Rayko_Image_Compare.py"
|
||||
"Rayko_Image_Prompt.py"
|
||||
"Rayko_Image_Selector.py"
|
||||
"Rayko_Image_Text.py"
|
||||
"Rayko_Image_To_Latent.py"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "ComfyUI_RaykoStudio"
|
||||
description = "Set of custom nodes for ComfyUI providing additional image processing capabilities"
|
||||
version = "0.34.1"
|
||||
version = "0.35.0"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["torch", "numpy", "Pillow>=10.0.0", "freetype-py>=2.5.1", "pycairo>=1.29.0", "scipy>=1.10.0"]
|
||||
|
||||
|
||||
@@ -0,0 +1,476 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "RaykoStudio.ImagePrompt",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name !== "RS_ImagePrompt") return;
|
||||
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
const result = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
|
||||
const node = this;
|
||||
|
||||
node.data = {
|
||||
selected_image: "",
|
||||
prompt: ""
|
||||
};
|
||||
|
||||
const MIN_WIDTH = 360;
|
||||
const MIN_HEIGHT = 460;
|
||||
const PREVIEW_FIXED_HEIGHT = 250;
|
||||
|
||||
node.MIN_WIDTH = MIN_WIDTH;
|
||||
node.MIN_HEIGHT = MIN_HEIGHT;
|
||||
node.setSize([MIN_WIDTH, MIN_HEIGHT]);
|
||||
|
||||
const w_image = node.widgets?.find(w => w.name === "image");
|
||||
const w_prompt = node.widgets?.find(w => w.name === "prompt_preview");
|
||||
|
||||
const syncWidget = (widget, dataKey) => {
|
||||
if (widget) {
|
||||
widget.value = node.data[dataKey];
|
||||
widget.serializeValue = () => node.data[dataKey];
|
||||
}
|
||||
};
|
||||
|
||||
if (node.widgets) {
|
||||
node.widgets.forEach(w => {
|
||||
if (w.name === "image" && w.value) node.data.selected_image = w.value;
|
||||
if (w.name === "prompt_preview" && w.value) node.data.prompt = w.value;
|
||||
|
||||
w.hidden = true;
|
||||
w.type = "hidden";
|
||||
w.computeSize = () => [0, 0];
|
||||
w.computedHeight = 0;
|
||||
w.draw = function () {};
|
||||
|
||||
if (w.element && w.element.parentNode) {
|
||||
w.element.parentNode.removeChild(w.element);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
syncWidget(w_image, "selected_image");
|
||||
syncWidget(w_prompt, "prompt");
|
||||
|
||||
node.syncData = function () {
|
||||
if (node.graph) {
|
||||
node.graph.setDirtyCanvas(true, true);
|
||||
node.graph.changeTracker?.dispatchEvent(new Event("change"));
|
||||
}
|
||||
};
|
||||
|
||||
node.imageLoaded = false;
|
||||
node.imageLoading = false;
|
||||
|
||||
let promptRequestId = 0;
|
||||
|
||||
const mainContainer = document.createElement("div");
|
||||
mainContainer.style.cssText = `
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const toast = document.createElement("div");
|
||||
toast.style.cssText = "position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); padding:6px 14px; border-radius:6px; color:#fff; font:bold 14px Arial; z-index:10; pointer-events:none; display:none; white-space:nowrap;";
|
||||
mainContainer.appendChild(toast);
|
||||
|
||||
let toastTimer = null;
|
||||
node.showToast = function (text, type) {
|
||||
let color, borderColor, borderWidth;
|
||||
if (type === "success") {
|
||||
color = "rgba(34, 197, 94, 0.97)";
|
||||
borderColor = "rgba(255, 255, 255, 0.95)";
|
||||
borderWidth = 2;
|
||||
} else if (type === "warning") {
|
||||
color = "rgba(249, 115, 22, 0.97)";
|
||||
borderColor = "rgba(220, 38, 38, 1)";
|
||||
borderWidth = 3;
|
||||
} else {
|
||||
color = "rgba(239, 68, 68, 0.97)";
|
||||
borderColor = "rgba(255, 255, 255, 0.95)";
|
||||
borderWidth = 2;
|
||||
}
|
||||
toast.textContent = text;
|
||||
toast.style.background = color;
|
||||
toast.style.border = `${borderWidth}px solid ${borderColor}`;
|
||||
toast.style.display = "block";
|
||||
if (toastTimer) clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => {
|
||||
toast.style.display = "none";
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const onDocumentDragOver = (e) => {
|
||||
if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
const onDocumentDrop = (e) => {
|
||||
if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
document.addEventListener('dragover', onDocumentDragOver);
|
||||
document.addEventListener('drop', onDocumentDrop);
|
||||
node._docDragOver = onDocumentDragOver;
|
||||
node._docDrop = onDocumentDrop;
|
||||
|
||||
const textContainer = document.createElement("div");
|
||||
textContainer.style.cssText = "width:100%; pointer-events:auto;";
|
||||
|
||||
const customTextArea = document.createElement("textarea");
|
||||
customTextArea.readOnly = true;
|
||||
customTextArea.value = node.data.prompt;
|
||||
customTextArea.placeholder = "Prompt will appear here after loading an image...";
|
||||
customTextArea.style.cssText = "width:100%; min-height:50px; padding:6px; background:#1e1e1e; color:#eee; border:1px solid #444; border-radius:4px; resize:none; font-family:monospace; font-size:12px; box-sizing:border-box; pointer-events:auto; cursor:text;";
|
||||
textContainer.appendChild(customTextArea);
|
||||
|
||||
const buttonsContainer = document.createElement("div");
|
||||
buttonsContainer.style.cssText = "display:flex; gap:6px; width:100%; pointer-events:auto;";
|
||||
|
||||
const btnUpload = document.createElement("button");
|
||||
btnUpload.textContent = "📂 UPLOAD IMAGE";
|
||||
btnUpload.style.cssText = "flex:3; height:28px; background:#2a2a2a; color:#4CAF50; border:2px solid #4CAF50; border-radius:4px; cursor:pointer; font-weight:normal; font-size:12px; transition:all 0.2s; pointer-events:auto;";
|
||||
btnUpload.onmouseover = () => { btnUpload.style.backgroundColor = "#3a3a3a"; };
|
||||
btnUpload.onmouseout = () => { btnUpload.style.backgroundColor = "#2a2a2a"; };
|
||||
btnUpload.onclick = () => node.triggerFileUpload();
|
||||
|
||||
const btnCopy = document.createElement("button");
|
||||
btnCopy.textContent = "📝 COPY";
|
||||
btnCopy.title = "Copy prompt to clipboard";
|
||||
btnCopy.style.cssText = "flex:1; height:28px; background:#2a2a2a; color:#64B5F6; border:2px solid #2196F3; border-radius:4px; cursor:pointer; font-weight:normal; font-size:12px; transition:all 0.2s; pointer-events:auto;";
|
||||
btnCopy.onmouseover = () => { if (customTextArea.value) btnCopy.style.backgroundColor = "#3a3a3a"; };
|
||||
btnCopy.onmouseout = () => { btnCopy.style.backgroundColor = "#2a2a2a"; };
|
||||
btnCopy.onclick = () => copyPrompt();
|
||||
|
||||
buttonsContainer.appendChild(btnUpload);
|
||||
buttonsContainer.appendChild(btnCopy);
|
||||
|
||||
const previewWrap = document.createElement("div");
|
||||
previewWrap.style.cssText = `
|
||||
width: 100%;
|
||||
height: ${PREVIEW_FIXED_HEIGHT}px;
|
||||
background: #353535;
|
||||
border: 1px dashed #555;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
const placeholder = document.createElement("div");
|
||||
placeholder.textContent = "📁 Drop Image Here or Click to Upload";
|
||||
placeholder.style.cssText = "color:#888; font-size:14px; font-family:Arial,sans-serif; text-align:center; pointer-events:none; user-select:none;";
|
||||
previewWrap.appendChild(placeholder);
|
||||
|
||||
const previewImg = document.createElement("img");
|
||||
previewImg.style.cssText = "display:none; width:100%; height:100%; object-fit:contain; pointer-events:none; user-select:none;";
|
||||
previewImg.alt = "Image preview";
|
||||
previewWrap.appendChild(previewImg);
|
||||
|
||||
mainContainer.appendChild(textContainer);
|
||||
mainContainer.appendChild(buttonsContainer);
|
||||
mainContainer.appendChild(previewWrap);
|
||||
|
||||
const copyPrompt = async () => {
|
||||
const text = customTextArea.value;
|
||||
if (!text) return;
|
||||
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
node.showToast("Copy done!", "success");
|
||||
return;
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
try {
|
||||
customTextArea.focus();
|
||||
customTextArea.select();
|
||||
const ok = document.execCommand("copy");
|
||||
customTextArea.setSelectionRange(0, 0);
|
||||
customTextArea.blur();
|
||||
if (ok) node.showToast("Copy done!", "success");
|
||||
else node.showToast("Copy error!", "error");
|
||||
} catch (e) {
|
||||
node.showToast("Copy error!", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const updateCopyBtnState = () => {
|
||||
if (!btnCopy) return;
|
||||
const has = !!customTextArea.value;
|
||||
btnCopy.style.opacity = has ? "1" : "0.4";
|
||||
btnCopy.style.cursor = has ? "pointer" : "not-allowed";
|
||||
};
|
||||
|
||||
const fetchPrompt = async (imagePath, notifyNotFound) => {
|
||||
if (!imagePath) return;
|
||||
|
||||
const requestId = ++promptRequestId;
|
||||
|
||||
node.data.prompt = "";
|
||||
customTextArea.value = "";
|
||||
customTextArea.placeholder = "⏳ Loading prompt...";
|
||||
syncWidget(w_prompt, "prompt");
|
||||
updateCopyBtnState();
|
||||
node.syncData();
|
||||
|
||||
try {
|
||||
const response = await fetch(`/rayko/get_prompt?filename=${encodeURIComponent(imagePath)}`);
|
||||
|
||||
if (requestId !== promptRequestId) return;
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (requestId !== promptRequestId) return;
|
||||
|
||||
node.data.prompt = result.prompt || "";
|
||||
customTextArea.value = node.data.prompt;
|
||||
customTextArea.placeholder = node.data.prompt
|
||||
? ""
|
||||
: "⚠ Prompt not found in this image";
|
||||
syncWidget(w_prompt, "prompt");
|
||||
updateCopyBtnState();
|
||||
node.syncData();
|
||||
|
||||
if (!node.data.prompt && notifyNotFound) {
|
||||
node.showToast("⚠ Prompt not found", "warning");
|
||||
}
|
||||
} catch (err) {
|
||||
if (requestId !== promptRequestId) return;
|
||||
customTextArea.placeholder = "⚠ Prompt not found in this image";
|
||||
}
|
||||
};
|
||||
|
||||
const buildViewUrl = (imagePath) => {
|
||||
let filename = imagePath;
|
||||
let subfolder = "";
|
||||
if (imagePath.includes("/")) {
|
||||
const parts = imagePath.split("/");
|
||||
subfolder = parts[0];
|
||||
filename = parts.slice(1).join("/");
|
||||
}
|
||||
let url = `/view?filename=${encodeURIComponent(filename)}&type=input`;
|
||||
if (subfolder) url += `&subfolder=${encodeURIComponent(subfolder)}`;
|
||||
return url + `&t=${Date.now()}`;
|
||||
};
|
||||
|
||||
node.loadImage = function (imagePath, notifyNotFound) {
|
||||
if (!imagePath || node.imageLoading) return;
|
||||
|
||||
node.imageLoading = true;
|
||||
node.data.selected_image = imagePath;
|
||||
syncWidget(w_image, "selected_image");
|
||||
|
||||
previewImg.onload = () => {
|
||||
node.imageLoading = false;
|
||||
node.imageLoaded = true;
|
||||
previewImg.style.display = "block";
|
||||
placeholder.style.display = "none";
|
||||
node.syncData();
|
||||
fetchPrompt(imagePath, notifyNotFound);
|
||||
};
|
||||
previewImg.onerror = () => {
|
||||
node.imageLoading = false;
|
||||
node.imageLoaded = false;
|
||||
previewImg.style.display = "none";
|
||||
placeholder.style.display = "block";
|
||||
node.syncData();
|
||||
};
|
||||
previewImg.src = buildViewUrl(imagePath);
|
||||
};
|
||||
|
||||
const uploadFileAndLoad = async (file) => {
|
||||
if (!file) return false;
|
||||
|
||||
const SUPPORTED_EXT = /\.(png|jpe?g|webp)$/i;
|
||||
const SUPPORTED_MIME = ['image/png', 'image/jpeg', 'image/webp'];
|
||||
const isSupported = SUPPORTED_EXT.test(file.name) || SUPPORTED_MIME.includes(file.type);
|
||||
|
||||
if (!isSupported) {
|
||||
node.showToast("⚠ Only PNG/JPG/WebP supported", "warning");
|
||||
return false;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
formData.append('subfolder', '');
|
||||
formData.append('type', 'input');
|
||||
|
||||
try {
|
||||
const response = await fetch('/upload/image', { method: 'POST', body: formData });
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
const imageName = result.name || result.filename;
|
||||
const subfolder = result.subfolder || '';
|
||||
const finalName = subfolder ? `${subfolder}/${imageName}` : imageName;
|
||||
node.loadImage(finalName, true);
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[RS Image-Prompt] Upload error:", err);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
node.triggerFileUpload = function () {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/png,image/jpeg,image/webp';
|
||||
fileInput.onchange = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
await uploadFileAndLoad(file);
|
||||
fileInput.remove();
|
||||
};
|
||||
fileInput.click();
|
||||
};
|
||||
|
||||
previewWrap.addEventListener('dragover', (e) => {
|
||||
if (!e.dataTransfer || !e.dataTransfer.types.includes('Files')) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
previewWrap.style.borderColor = '#4CAF50';
|
||||
previewWrap.style.backgroundColor = '#2a2a2a';
|
||||
});
|
||||
|
||||
previewWrap.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
previewWrap.style.borderColor = '#555';
|
||||
previewWrap.style.backgroundColor = '#353535';
|
||||
});
|
||||
|
||||
previewWrap.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
previewWrap.style.borderColor = '#555';
|
||||
previewWrap.style.backgroundColor = '#353535';
|
||||
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (file) {
|
||||
uploadFileAndLoad(file);
|
||||
}
|
||||
});
|
||||
|
||||
previewWrap.addEventListener('click', () => {
|
||||
node.triggerFileUpload();
|
||||
});
|
||||
|
||||
node.addDOMWidget("custom_widgets", "customtext", mainContainer);
|
||||
node.customTextArea = customTextArea;
|
||||
|
||||
const updateUIFromData = () => {
|
||||
customTextArea.value = node.data.prompt || "";
|
||||
updateCopyBtnState();
|
||||
};
|
||||
|
||||
node.onSerialize = function (o) {
|
||||
syncWidget(w_image, "selected_image");
|
||||
syncWidget(w_prompt, "prompt");
|
||||
o.data = node.data;
|
||||
};
|
||||
|
||||
node.onConfigure = function (o) {
|
||||
if (o && o.data) {
|
||||
node.data = { ...node.data, ...o.data };
|
||||
}
|
||||
|
||||
node.imageLoading = false;
|
||||
node.imageLoaded = false;
|
||||
|
||||
if (node.data.selected_image) {
|
||||
node.loadImage(node.data.selected_image);
|
||||
}
|
||||
|
||||
updateUIFromData();
|
||||
|
||||
syncWidget(w_image, "selected_image");
|
||||
syncWidget(w_prompt, "prompt");
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (node.size) {
|
||||
node.onResize([...node.size]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
node.visibilityHandler = function () {
|
||||
if (!document.hidden && node.graph) {
|
||||
node.graph.setDirtyCanvas(true, true);
|
||||
node.syncData();
|
||||
|
||||
if (node.size) {
|
||||
node.onResize([...node.size]);
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener("visibilitychange", node.visibilityHandler);
|
||||
|
||||
const originalOnResize = node.onResize;
|
||||
node.onResize = function (size) {
|
||||
if (size[0] < MIN_WIDTH) size[0] = MIN_WIDTH;
|
||||
if (size[1] < MIN_HEIGHT) size[1] = MIN_HEIGHT;
|
||||
|
||||
const titleBarHeight = 30;
|
||||
const topPadding = 18;
|
||||
const gap = 8;
|
||||
const bottomPadding = 20;
|
||||
const previewSpacing = 20;
|
||||
|
||||
const fixedElements = titleBarHeight + topPadding +
|
||||
28 + gap +
|
||||
previewSpacing +
|
||||
PREVIEW_FIXED_HEIGHT +
|
||||
bottomPadding;
|
||||
|
||||
const textareaHeight = Math.max(50, size[1] - fixedElements);
|
||||
customTextArea.style.height = textareaHeight + 'px';
|
||||
|
||||
if (originalOnResize) originalOnResize.apply(this, arguments);
|
||||
node.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
const originalOnRemoved = node.onRemoved;
|
||||
node.onRemoved = function () {
|
||||
document.removeEventListener("visibilitychange", node.visibilityHandler);
|
||||
if (node._docDragOver) {
|
||||
document.removeEventListener('dragover', node._docDragOver);
|
||||
}
|
||||
if (node._docDrop) {
|
||||
document.removeEventListener('drop', node._docDrop);
|
||||
}
|
||||
if (originalOnRemoved) originalOnRemoved.apply(this, arguments);
|
||||
};
|
||||
|
||||
if (node.data.selected_image) {
|
||||
node.loadImage(node.data.selected_image);
|
||||
}
|
||||
|
||||
updateCopyBtnState();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
node.onResize([...node.size]);
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
},
|
||||
setup() {}
|
||||
});
|
||||
Reference in New Issue
Block a user