とりあえずまとめた

This commit is contained in:
da2el-ai
2024-10-01 12:58:40 +09:00
commit 6d2af935d2
12 changed files with 851 additions and 0 deletions
+40
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
__pycache__
+27
View File
@@ -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"
},
}
+21
View File
@@ -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.
+11
View File
@@ -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"]
+19
View File
@@ -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
+480
View File
@@ -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,
}
+55
View File
@@ -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
+33
View File
@@ -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
+77
View File
@@ -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;
},
};
},
});
+41
View File
@@ -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);
};
},
});
+46
View File
@@ -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);
};
},
});