commit 6d2af935d2f831791cc282bf23c2da364ef976fe Author: da2el-ai Date: Tue Oct 1 12:58:40 2024 +0900 とりあえずまとめた diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..141fb36 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,40 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +# Use 4 spaces for the Python files +[*.py] +indent_size = 4 +max_line_length = 120 + +# The JSON files contain newlines inconsistently +[*.json] +insert_final_newline = ignore + +[*.js] +indent_size = 4 +max_line_length = 120 + +# Minified JavaScript files shouldn't be changed +[**.min.js] +indent_style = ignore +insert_final_newline = ignore + +# Makefiles always use tabs for indentation +[Makefile] +indent_style = tab + +# Batch files use tabs for indentation +[*.bat] +indent_style = tab + +[*.md] +trim_trailing_whitespace = false + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4d07118 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..dac730d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,27 @@ +{ + "javascript.format.insertSpaceBeforeFunctionParenthesis": true, + "typescript.format.insertSpaceBeforeFunctionParenthesis": true, + "editor.tabSize": 4, + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.fixAll": "explicit" + }, + // eslint + // 適応するファイルタイプを決定 + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "vue" + ], + // プロジェクト配下のeslintrc.jsを読み込み + "eslint.options": { + "overrideConfigFile": ".eslintrc.js" + }, + "eslint.workingDirectories": ["./"], + "files.associations": { + "*.vue": "vue" + }, +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ac1b5cd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Shingo.T + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e75b375 --- /dev/null +++ b/__init__.py @@ -0,0 +1,11 @@ +""" +@author: da2el +@title: D2 Nodes +@description: A Collection of Handy Custom Nodes for ComfyUI +""" + +from .d2_nodes import NODE_CLASS_MAPPINGS + +WEB_DIRECTORY = "./web" + +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] diff --git a/config/sizeselector_config.sample.yaml b/config/sizeselector_config.sample.yaml new file mode 100644 index 0000000..a459342 --- /dev/null +++ b/config/sizeselector_config.sample.yaml @@ -0,0 +1,19 @@ +size_dict: + 512 x 512: + width: 512 + height: 512 + 512 x 768: + width: 512 + height: 768 + 1024 x 1024: + width: 1024 + height: 1024 + 768 x 1024: + width: 768 + height: 1024 + 832 x 1216: + width: 832 + height: 1216 + 1248 x 1632: + width: 1248 + height: 1632 diff --git a/d2_nodes.py b/d2_nodes.py new file mode 100644 index 0000000..9866290 --- /dev/null +++ b/d2_nodes.py @@ -0,0 +1,480 @@ +import torch +import math +import os +import json +import hashlib +import folder_paths +import comfy.sd +import latent_preview +import re +import random + +import comfy.samplers +from comfy.cli_args import args + +from nodes import common_ksampler, CLIPTextEncode, PreviewImage + +from .modules import util +from .modules import checkpoint_util + + +MAX_SEED = 2**32 - 1 # 4,294,967,295 + + +""" + +D2 KSampler +positive / negative 入力に文字列が使える KSampler + +""" +class D2_KSampler: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model": ("MODEL",), + "clip": ("CLIP",), + "vae": ("VAE",), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), + "cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0}), + "sampler_name": (comfy.samplers.KSampler.SAMPLERS, ), + "scheduler": (comfy.samplers.KSampler.SCHEDULERS, ), + "latent_image": ("LATENT",), + "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + "preview_method": (["auto", "latent2rgb", "taesd", "vae_decoded_only", "none"],), + "positive": ("STRING", {"default": "","multiline": True}), + "negative": ("STRING", {"default": "", "multiline": True}), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID",}, + } + + RETURN_TYPES = ("IMAGE", "STRING", "STRING", ) + RETURN_NAMES = ("IMAGE", "positive", "negative") + OUTPUT_NODE = True + FUNCTION = "run" + CATEGORY = "D2" + + def run(self, model, clip, vae, seed, steps, cfg, sampler_name, scheduler, latent_image, denoise, + preview_method, positive, negative, prompt=None, extra_pnginfo=None, my_unique_id=None): + + self._set_preview_method(preview_method) + + (positive_encoded,) = CLIPTextEncode().encode(clip, positive) + (negative_encoded,) = CLIPTextEncode().encode(clip, negative) + + sampler_result = common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive_encoded, negative_encoded, latent_image, denoise=denoise) + latent = sampler_result[0]['samples'] + samp_images = vae.decode(latent).cpu() + results_images = PreviewImage().save_images(samp_images, "d2", prompt, extra_pnginfo)['ui']['images'] + + return { + "ui": {"images": results_images}, + "result": (samp_images, positive, negative,) + } + + + def _set_preview_method(self, method): + if method == 'auto' or method == 'LatentPreviewMethod.Auto': + args.preview_method = latent_preview.LatentPreviewMethod.Auto + elif method == 'latent2rgb' or method == 'LatentPreviewMethod.Latent2RGB': + args.preview_method = latent_preview.LatentPreviewMethod.Latent2RGB + elif method == 'taesd' or method == 'LatentPreviewMethod.TAESD': + args.preview_method = latent_preview.LatentPreviewMethod.TAESD + else: + args.preview_method = latent_preview.LatentPreviewMethod.NoPreviews + + + +""" + +D2_CheckpointLoader +Checkpointのフルパスを取得できる Checkpoint Loader + +""" +class D2_CheckpointLoader: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "ckpt_name": (folder_paths.get_filename_list("checkpoints"), ), + }, + "hidden": { + "unique_id": "UNIQUE_ID", "extra_pnginfo": "EXTRA_PNGINFO", "prompt": "PROMPT"} + } + + RETURN_TYPES = ("MODEL", "CLIP", "VAE", "STRING", "STRING", "STRING") + RETURN_NAMES = ("model", "clip", "vae", "ckpt_name", "ckpt_hash", "ckpt_fullpath") + FUNCTION = "load_checkpoint" + + CATEGORY = "D2" + + def load_checkpoint(self, ckpt_name, output_vae=True, output_clip=True, unique_id=None, extra_pnginfo=None, prompt=None): + ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name) + out = comfy.sd.load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, embedding_directory=folder_paths.get_folder_paths("embeddings")) + hash = checkpoint_util.get_file_hash(ckpt_path) + ckpt_name = os.path.basename(ckpt_name) + return out[:3] + (ckpt_name, hash, ckpt_path) + + + +""" + +D2 RegexSwitcher +正規表現で検索して文字列を結合・出力するノード + +""" +class D2_RegexSwitcher: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + # 検索対象テキスト + "text": ( + "STRING", {"forceInput": True, "multiline": True, "default": ""}, + ), + # 先頭に結合するテキスト + "prefix": ( + "STRING", {"forceInput": True, "multiline": True, "default":"",}, + ), + # 最後に結合するテキスト + "suffix": ( + "STRING", {"forceInput": True, "multiline": True, "default":"",}, + ), + # 正規表現、出力テキストのペア + "regex_and_output": ( + "STRING", {"multiline": True, "default": "pony\n--\nscore_9,\n--\n--\nhighres, high quality,"}, + ), + }, + } + + RETURN_TYPES = ("STRING", "STRING", "STRING", "INT",) + RETURN_NAMES = ("combined_text", "prefix", "suffix", "index",) + FUNCTION = "run" + CATEGORY = "D2" + + ###### + def run(self, text, prefix, suffix, regex_and_output): + """ + 正規表現に基づいてテキストをマッチングし、結果を結合して返す関数。 + + Args: + text (str): マッチング対象のテキスト + pony + -- + score_9, + -- + -- + highres, high quality, + prefix (str): 結果の前に付加するテキスト + suffix (str): 結果の後に付加するテキスト + regex_and_output (str): 正規表現とその出力のペアを "--" で区切った文字列 + + Returns: + dict: UI用のテキストと結果のタプルを含む辞書 + """ + # regex_and_output を -- で分割し、ペアにする + pairs = regex_and_output.split('--') + + # ペアをリストに整理する + regex_output_list = [] + default_output = None + for i in range(0, len(pairs), 2): + if i + 1 < len(pairs): + regex = pairs[i].strip() + output = pairs[i+1].strip() + if regex: + regex_output_list.append({ + 'regex': regex, + 'output': output + }) + else: + default_output = output + + match_text = "" + match_index = -1 + + # 各正規表現をチェックし、マッチしたら対応する出力を返す + for index, item in enumerate(regex_output_list): + if re.search(item['regex'], text, re.IGNORECASE): + match_text = item['output'] + match_index = index + + # マッチしなかった場合はデフォルト出力を返す + if match_index == -1: + match_text = default_output + + # 文字列を結合 + combined_text = f"{prefix}{match_text}{suffix}" + + return { + "ui": {"text": text}, + "result": (combined_text, prefix, suffix, match_index) + } + + + +""" + +D2 PromptSR +入力された文字列を書き換えて LIST 出力するノード +qq-nodes-comfyui の XY Plot で使用 + +""" +class D2_PromptSR: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + # プロンプト + "prompt": ( + "STRING", {"multiline": True}, + ), + # 検索ワード + "search_txt": ( + "STRING", {"multiline": False}, + ), + # 置換文字列 + "replace": ( + "STRING", {"multiline": True}, + ), + }, + } + + RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("LIST",) + FUNCTION = "replace_text" + CATEGORY = "D2" + + def replace_text(self, prompt, search_txt, replace): + # 置換文字列を改行で分割 + replace_options = replace.strip().split('\n') + + # 出力リスト + output_list = [prompt] + + # 文字列を検索して置換 + for option in replace_options: + new_prompt = prompt.replace(search_txt, option) + output_list.append(new_prompt) + + return (output_list,) + + + +""" + +D2 MultiOutput +数値、文字列、SEEDのリストを出力するノード + +""" +class D2_MultiOutput: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + # 入力タイプ + "type": (["FLOAT","INT","STRING","SEED",],), + # プロンプト + "parameter": ( + "STRING",{"multiline": True}, + ), + }, + "optional": { + "reset": ("D2RESET", {}) + } + } + + RETURN_TYPES = ("LIST",) + RETURN_NAMES = ("LIST",) + FUNCTION = "output_list" + CATEGORY = "D2" + + ###### + # def output_list(self, type, parameter, seed): + def output_list(self, type, parameter, reset = ""): + + # 入力文字列を改行で分割 + param_options = parameter.strip().split('\n') + + # 出力リスト + output_list = [] + + # 文字列を検索して置換 + for option in param_options: + if type == "INT" or type == "SEED": + output_list.append(int(option)) + elif type == "FLOAT": + output_list.append(float(option)) + else: + output_list.append(option) + + return (output_list,) + + + +""" + +D2 SizeSelector +画像サイズセレクター +指定サイズの latent も取得できる + +""" +class D2_SizeSelector: + + @classmethod + def INPUT_TYPES(cls): + # 設定を読む + config_path = util.get_config_path("sizeselector_config.yaml") + config_sample_path = util.get_config_path("sizeselector_config.sample.yaml") + config_value = util.load_config(config_path, config_sample_path) + + cls.size_dict = config_value["size_dict"] + cls.size_list = ["custom"] + cls.size_list.extend(cls.size_dict.keys()) + + return { + "required": { + "preset": (cls.size_list,), + "width": ("INT", {"default": 1024, "min": 64, "max": 8192}), + "height": ("INT", {"default": 1024, "min": 64, "max": 8192}), + "swap_dimensions": (["Off", "On"],), + "upscale_factor": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 100.0, "step":0.1}), + "prescale_factor": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 100.0, "step":0.1}), + "batch_size": ("INT", {"default": 1, "min": 1, "max": 64}) + } + } + + RETURN_TYPES = ("INT", "INT", "FLOAT", "FLOAT", "INT", "LATENT",) + RETURN_NAMES = ("width", "height", "upscale_factor", "prescale_factor", "batch_size", "empty_latent",) + FUNCTION = "run" + CATEGORY = "D2" + + def run(self, preset, width, height, swap_dimensions, upscale_factor, prescale_factor, batch_size): + + if(preset != "custom"): + width = self.__class__.size_dict.get(preset).get("width", width) + height = self.__class__.size_dict.get(preset).get("height", height) + + if swap_dimensions == "On": + width, height = height, width + + width = int(width*prescale_factor) + height = int(height*prescale_factor) + + latent = torch.zeros([batch_size, 4, height // 8, width // 8]) + + return(width, height, upscale_factor, prescale_factor, batch_size, {"samples":latent}, ) + + + +""" + +D2_RefinerSteps +Refinerの切り替えステップをステップ数で指定する + +""" +class D2_RefinerSteps: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "steps": ("INT", {"default": 25, "min":0}), + "start": ("INT", {"default": 0, "min":0}), + "end": ("INT", {"default": 5, "min":0}), + } + } + RETURN_TYPES = ("INT", "INT", "INT", "INT",) + RETURN_NAMES = ("steps", "start", "end", "refiner_start",) + FUNCTION = "run" + CATEGORY = "D2" + + def run(self, steps, start, end): + refiner_start = end + 1 + return(steps, start, end, refiner_start,) + + + +""" + +D2 Refiner Steps A1111 +Refinerの切り替えステップを%で指定する + +""" +class D2_RefinerStepsA1111: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "steps": ("INT", {"default": 25}), + "denoise": ("FLOAT", {"default": 1, "min":0, "max":1, "step":0.01}), + "switch_at": ("FLOAT", {"default": 0.2, "min":0, "max":1, "step":0.01}), + } + } + RETURN_TYPES = ("INT", "INT", "INT", "INT",) + RETURN_NAMES = ("steps", "start", "end", "refiner_start",) + FUNCTION = "run" + CATEGORY = "D2" + + def run(self, steps, denoise, switch_at): + real_steps = math.floor(steps / denoise) + start = real_steps - steps + end = math.floor((real_steps - start) * switch_at) + start + refiner_start = end + 1 + + return(real_steps, start, end, refiner_start,) + + + + +""" + +D2 Refiner Steps Tester +Refiner Steps の計算結果を確認するノード + +""" +class D2_RefinerStepsTester: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "steps": ("INT", {"forceInput":True}), + }, + "optional": { + "start": ("INT", {"forceInput":True}), + "end": ("INT", {"forceInput":True}), + "refiner_start": ("INT", {"forceInput":True}), + } + } + + # INPUT_IS_LIST = True + RETURN_TYPES = () + OUTPUT_NODE = True + FUNCTION = "run" + CATEGORY = "D2" + + def run(self, steps=0, start=0, end=0, refiner_start=0): + text = f"stesps: {steps}\nstart: {start}\nend: {end}\nrefiner_start: {refiner_start}" + return {"ui": {"text": text}, "result": (text,)} + + + + +NODE_CLASS_MAPPINGS = { + "D2 KSampler": D2_KSampler, + "D2 Checkpoint Loader": D2_CheckpointLoader, + "D2 Regex Switcher": D2_RegexSwitcher, + "D2 Prompt SR": D2_PromptSR, + "D2 Multi Output": D2_MultiOutput, + "D2 Size Slector": D2_SizeSelector, + "D2 Refiner Steps": D2_RefinerSteps, + "D2 Refiner Steps A1111": D2_RefinerStepsA1111, + "D2 Refiner Steps Tester": D2_RefinerStepsTester, +} + + diff --git a/modules/checkpoint_util.py b/modules/checkpoint_util.py new file mode 100644 index 0000000..99f3dc7 --- /dev/null +++ b/modules/checkpoint_util.py @@ -0,0 +1,55 @@ +# ほとんどのコードは mikey_nodes のものを使っています +# https://github.com/bash-j/mikey_nodes + +import os +import json +import hashlib +import folder_paths + + +def calculate_file_hash(file_path): + # open the file in binary mode + with open(file_path, 'rb') as f: + # read the file in chunks to avoid loading the whole file into memory + chunk_size = 4096 + hash_object = hashlib.sha256() + while True: + chunk = f.read(chunk_size) + if not chunk: + break + hash_object.update(chunk) + # return the hexadecimal representation of the hash + return hash_object.hexdigest() + +def get_cached_file_hashes(): + # load the cached file hashes from the JSON file + cache_file_path = os.path.join(folder_paths.base_path, 'file_hashes.json') + if os.path.exists(cache_file_path): + with open(cache_file_path, 'r') as f: + return json.load(f) + else: + return {} + +def cache_file_hash(file_path, file_hash): + # update the cached file hashes dictionary and save to the JSON file + cache_file_path = os.path.join(folder_paths.base_path, 'file_hashes.json') + cached_file_hashes = get_cached_file_hashes() + cached_file_hashes[os.path.basename(file_path)] = file_hash + with open(cache_file_path, 'w') as f: + json.dump(cached_file_hashes, f) + +def get_file_hash(file_path): + # check if the file hash is already cached + # replace \ with / in file_path + file_path = file_path.replace('\\', '/') + cached_file_hashes = get_cached_file_hashes() + file_name = os.path.basename(file_path) + if file_name in cached_file_hashes: + return cached_file_hashes[file_name] + else: + # calculate the file hash and cache it + file_hash = calculate_file_hash(file_path)[:10] + cache_file_hash(file_path, file_hash) + return file_hash + + diff --git a/modules/util.py b/modules/util.py new file mode 100644 index 0000000..a1e4bb9 --- /dev/null +++ b/modules/util.py @@ -0,0 +1,33 @@ +import os +import yaml +import shutil +from pathlib import Path + +""" +設定ファイルを読み込む +ファイルがなければ見本を複製する +""" +def load_config(config_path:str, sample_path:str): + if not os.path.exists(config_path): + shutil.copy2(sample_path, config_path) + + with open(config_path, "r", encoding="utf-8") as file: + return yaml.safe_load(file) + + + +""" +ルートディレクトリ取得 +""" +def get_root_path(): + return Path(__file__).resolve().parents[1] + + + +""" +設定ファイルのフルパスを取得 +""" +def get_config_path(filename): + config_path = get_root_path() / 'config' + return config_path / filename + diff --git a/web/D2_MultiOutput.js b/web/D2_MultiOutput.js new file mode 100644 index 0000000..c7db004 --- /dev/null +++ b/web/D2_MultiOutput.js @@ -0,0 +1,77 @@ +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; +import { ComfyWidgets } from "/scripts/widgets.js"; + +const BUTTON_NAME = "Add Random"; +const MAX_SEED = 2 ** 32 - 1; // 4,294,967,295 + +class D2_MultiOutputClass { + btnWidget = undefined; + typeWidget = undefined; + inputWidget = undefined; + + // seed生成ボタンの表示切り替え + changeBtnVisible(value) { + if (value === "SEED") { + this.btnWidget.type = "button"; + } else { + this.btnWidget.type = "converted-widget"; + } + } +} + +const d2mo = new D2_MultiOutputClass(); + +app.registerExtension({ + name: "Comfy.D2.D2_MultiOutput", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name !== "D2 Multi Output") return; + + const origOnNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + const r = origOnNodeCreated ? origOnNodeCreated.apply(this) : undefined; + + d2mo.btnWidget = this.widgets?.find((w) => w.name === BUTTON_NAME); + d2mo.typeWidget = this.widgets?.find((w) => w.name === "type"); + d2mo.inputWidget = this.widgets?.find((w) => w.name === "parameter"); + + // seed生成ボタンの表示切り替え + if (d2mo.typeWidget !== undefined) { + d2mo.typeWidget.callback = (value) => { + d2mo.changeBtnVisible(value); + }; + + setTimeout(() => { + d2mo.changeBtnVisible(d2mo.typeWidget.value); + }, 200); + } + + // const r = origOnNodeCreated + // ? origOnNodeCreated.apply(this) + // : undefined; + // for (const w of this.widgets) { + // if (w.name === "seed") { + // w.type = "converted-widget"; + // if (!w.linkedWidgets) continue; + // for (const lw of w.linkedWidgets) { + // lw.type = "converted-widget"; + // } + // } + // } + return r; + }; + }, + getCustomWidgets(app) { + return { + D2RESET(node, inputName, inputData, app) { + const widget = node.addWidget("button", BUTTON_NAME, 0, () => { + console.log("click reset"); + const seed = Math.floor(Math.random() * MAX_SEED); + d2mo.inputWidget.value += d2mo.inputWidget.value.length >= 1 ? "\n" : ""; + d2mo.inputWidget.value += seed; + }); + return widget; + }, + }; + }, +}); diff --git a/web/D2_RefinerStepsTester.js b/web/D2_RefinerStepsTester.js new file mode 100644 index 0000000..03dbfb8 --- /dev/null +++ b/web/D2_RefinerStepsTester.js @@ -0,0 +1,41 @@ +import { app } from "/scripts/app.js"; +import { ComfyWidgets } from "/scripts/widgets.js"; + +app.registerExtension({ + name: "Comfy.D2.D2_RefinerStepsTester", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name !== "D2 Refiner Steps Tester") return; + + // 以下、カスタムノード処理 + function populate(text) { + // すでに表示しているものがあれば削除 + if (this.widgets) { + this.widgets.forEach((widget) => { + widget.onRemove?.(); + }); + this.widgets.length = 0; + } + + // 文字列はなぜか配列で送られてくるので結合 + const showText = text.join(""); + const widget = ComfyWidgets["STRING"]( + this, + "text", + ["STRING", { multiline: true }], + app + ).widget; + widget.inputEl.readOnly = true; + widget.inputEl.style.opacity = 0.6; + widget.value = showText; + } + + /** + * ノード実行時 + */ + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (message) { + onExecuted?.apply(this, arguments); + populate.call(this, message.text); + }; + }, +}); diff --git a/web/D2_RegexSwitcher.js b/web/D2_RegexSwitcher.js new file mode 100644 index 0000000..5c4f2ff --- /dev/null +++ b/web/D2_RegexSwitcher.js @@ -0,0 +1,46 @@ +import { app } from "/scripts/app.js"; +import { ComfyWidgets } from "/scripts/widgets.js"; + +app.registerExtension({ + name: "Comfy.D2.D2_RegexSwitcher", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name !== "D2 Regex Switcher") return; + + // 以下、カスタムノード処理 + function populate(text) { + // 確認用ウィジェットがすでにあれば削除、なければ作る + // すでに表示しているものがあれば削除 + if (this.widgets) { + let index = this.widgets.findIndex((widget) => widget.name === "text_check"); + + if (index >= 0) { + const widget = this.widgets[index]; + this.widgets.splice(index, 1); + widget?.onRemove(); + } + + const newWidget = ComfyWidgets["STRING"]( + this, + "text_check", + ["STRING", { multiline: true }], + app + ).widget; + newWidget.inputEl.readOnly = true; + newWidget.inputEl.style.opacity = 0.6; + + // 文字列はなぜか配列で送られてくるので結合 + const showText = text.join(""); + newWidget.value = showText; + } + } + + /** + * ノード実行時 + */ + const onExecuted = nodeType.prototype.onExecuted; + nodeType.prototype.onExecuted = function (message) { + onExecuted?.apply(this, arguments); + populate.call(this, message.text); + }; + }, +});