Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6083981758 | ||
|
|
2498771a3f | ||
|
|
ed0e3e3c82 | ||
|
|
f709533cd9 | ||
|
|
dea07a10d7 | ||
|
|
49e0b66089 | ||
|
|
aed718825e | ||
|
|
725552ab7a | ||
|
|
5fb2cc1bfd | ||
|
|
01a4b14137 | ||
|
|
e0b0e13172 | ||
|
|
e2060a548d | ||
|
|
64655aa8eb | ||
|
|
421771e1a5 | ||
|
|
18f91f9715 | ||
|
|
d4e254578c | ||
|
|
6db61b4bb8 | ||
|
|
d53a19118f | ||
|
|
49a784c023 | ||
|
|
df99ba7ffa | ||
|
|
664cd77564 | ||
|
|
3fded06dbe | ||
|
|
9040eb4b92 | ||
|
|
17b141d66c | ||
|
|
a85f5b6b96 | ||
|
|
42e5d50aa6 | ||
|
|
6115649c29 | ||
|
|
f1f28c6c93 | ||
|
|
6be8081f57 | ||
|
|
299888fdf8 |
@@ -0,0 +1,24 @@
|
||||
name: Publish to Comfy registry
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
jobs:
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
# if this is a forked repository. Skipping the workflow.
|
||||
if: github.event.repository.fork == false
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@main
|
||||
with:
|
||||
## Add your own personal access token to your Github Repository secrets and reference it here.
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||
@@ -158,3 +158,4 @@ cython_debug/
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
config.yaml
|
||||
|
||||
Vendored
+2
@@ -24,4 +24,6 @@
|
||||
"files.associations": {
|
||||
"*.vue": "vue"
|
||||
},
|
||||
"python.analysis.extraPaths": ["."],
|
||||
"python.autoComplete.extraPaths": ["."]
|
||||
}
|
||||
|
||||
+147
-89
@@ -1,7 +1,8 @@
|
||||
import json
|
||||
import os
|
||||
import numpy as np
|
||||
import json
|
||||
from typing import Dict, Optional, TypedDict, Union, List
|
||||
from typing import Dict, Optional, Literal
|
||||
|
||||
from PIL import Image
|
||||
from PIL.PngImagePlugin import PngInfo
|
||||
@@ -11,22 +12,12 @@ import folder_paths
|
||||
|
||||
from .modules.util import util
|
||||
from .modules.eagle_api import EagleAPI
|
||||
from .modules.prompt_info_extractor import PromptInfoExtractor
|
||||
from .modules.params_extractor import ParamsExtractor
|
||||
|
||||
from .my_types import TNodeParams, TGenInfo, D2_TD2Pipe
|
||||
|
||||
FORCE_WRITE_PROMPT = False
|
||||
|
||||
class ParamsInfo(TypedDict):
|
||||
format: str
|
||||
lossless_webp: bool
|
||||
save_tags: bool
|
||||
filename_template: str
|
||||
eagle_folder: str
|
||||
compression: int
|
||||
positive: str
|
||||
negative: str
|
||||
memo_text: str
|
||||
prompt: Optional[Dict]
|
||||
extra_pnginfo: Optional[Dict]
|
||||
|
||||
|
||||
class D2_SendEagle:
|
||||
@@ -35,41 +26,49 @@ class D2_SendEagle:
|
||||
self.type = "output"
|
||||
self.output_folder = ""
|
||||
self.subfolder_name = ""
|
||||
self.eagle_api = None
|
||||
self.eagle_api:EagleAPI = EagleAPI()
|
||||
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"format": (["webp", "png"],),
|
||||
"format": (["webp", "png", "jpeg"],),
|
||||
# webpの時に可逆(lossless)不可逆(lossy)どちらにするか
|
||||
"lossless_webp": (
|
||||
"BOOLEAN",
|
||||
{"default": True, "label_on": "lossless", "label_off": "lossy"},
|
||||
),
|
||||
# lossy の時の圧縮率
|
||||
# webp lossy または jpeg の時の圧縮率
|
||||
"compression": (
|
||||
"INT",
|
||||
{"default": 80, "min": 1, "max": 100, "step": 1},
|
||||
),
|
||||
# プロンプトをEagleタグに保存するか
|
||||
"save_tags": (
|
||||
"BOOLEAN",
|
||||
{"default": False, "label_on": "save", "label_off": "none"},
|
||||
{"default": 90, "min": 1, "max": 100, "step": 1},
|
||||
),
|
||||
# プロンプトやモデルをEagleタグに保存するか
|
||||
"save_tags": ([
|
||||
"None",
|
||||
"Prompt + Checkpoint",
|
||||
"Prompt",
|
||||
"Checkpoint",
|
||||
],),
|
||||
# 保存するファイル名
|
||||
"filename_template": (
|
||||
"STRING",
|
||||
{"multiline": False, "default":"{model}-{width}-{height}-{seed}"},
|
||||
{"multiline": False, "default":"{model}-{seed}"},
|
||||
),
|
||||
# Eagleフォルダ
|
||||
"eagle_folder": (
|
||||
"STRING",
|
||||
{"default": ""}
|
||||
),
|
||||
|
||||
# プレビュー表示するか
|
||||
"preview": (
|
||||
"BOOLEAN",
|
||||
{"default": True, "label_on": "ON", "label_off": "OFF"},
|
||||
),
|
||||
},
|
||||
"optional":{
|
||||
# ポジティブプロンプト
|
||||
"positive": (
|
||||
"STRING",
|
||||
@@ -85,76 +84,107 @@ class D2_SendEagle:
|
||||
"STRING",
|
||||
{"multiline": True},
|
||||
),
|
||||
"d2_pipe": ("D2_TD2Pipe",),
|
||||
},
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
RETURN_TYPES = ("STRING", "STRING", "IMAGE")
|
||||
RETURN_NAMES = ("positive", "negative", "IMAGE", )
|
||||
FUNCTION = "add_item"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "D2"
|
||||
|
||||
# ######################
|
||||
# ノード実行でEagleに画像を送る
|
||||
# ノード実行で呼ばれる関数
|
||||
# Eagleに画像を送る
|
||||
def add_item(
|
||||
self,
|
||||
images,
|
||||
format = "webp",
|
||||
lossless_webp = False,
|
||||
save_tags = True,
|
||||
save_tags = "None",
|
||||
filename_template = "{model}-{width}-{height}-{seed}",
|
||||
eagle_folder = "",
|
||||
compression = 80,
|
||||
positive = "",
|
||||
negative = "",
|
||||
preview = True,
|
||||
memo_text = "",
|
||||
d2_pipe: Optional[D2_TD2Pipe] = None,
|
||||
prompt: Optional[Dict] = None,
|
||||
extra_pnginfo: Optional[Dict] = None,
|
||||
):
|
||||
self.output_folder, self.subfolder_name = self.get_output_folder()
|
||||
self.eagle_api = EagleAPI()
|
||||
|
||||
results = list()
|
||||
params: ParamsInfo = {
|
||||
"format": format,
|
||||
"lossless_webp": lossless_webp,
|
||||
"save_tags": save_tags,
|
||||
"filename_template": filename_template,
|
||||
"eagle_folder": eagle_folder,
|
||||
"compression": compression,
|
||||
"positive": positive,
|
||||
"negative": negative,
|
||||
"memo_text": memo_text,
|
||||
"prompt": prompt,
|
||||
"extra_pnginfo": extra_pnginfo,
|
||||
}
|
||||
params = TNodeParams(
|
||||
format = format,
|
||||
lossless_webp = lossless_webp,
|
||||
save_tags = save_tags,
|
||||
filename_template = filename_template,
|
||||
eagle_folder = eagle_folder,
|
||||
compression = compression,
|
||||
positive = self.__class__.get_prompt_value("positive", positive, d2_pipe),
|
||||
negative = self.__class__.get_prompt_value("negative", negative, d2_pipe),
|
||||
memo_text = memo_text,
|
||||
prompt = prompt,
|
||||
extra_pnginfo = extra_pnginfo,
|
||||
)
|
||||
|
||||
for image in images:
|
||||
results.append(self.create_image_object(image, params))
|
||||
results.append(self.create_image_object(image, params, d2_pipe))
|
||||
|
||||
return {"ui": {"images": results}}
|
||||
if(preview):
|
||||
return {
|
||||
"ui": {"images": results},
|
||||
"result": (params["positive"], params["negative"], images,)
|
||||
}
|
||||
|
||||
return {
|
||||
"result": (params["positive"], params["negative"], images,)
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_prompt_value(cls, mode: Literal["positive","negative"], value: Optional[str], d2_pipe: Optional[D2_TD2Pipe]) -> str:
|
||||
if value:
|
||||
return value
|
||||
if d2_pipe is not None and getattr(d2_pipe, mode) is not None:
|
||||
return getattr(d2_pipe, mode)
|
||||
return ""
|
||||
|
||||
# ######################
|
||||
# イメージオブジェクトを作成
|
||||
def create_image_object(self, image, params:ParamsInfo) -> dict:
|
||||
def create_image_object(self, image, params:TNodeParams, d2_pipe:D2_TD2Pipe | None) -> dict:
|
||||
normalized_pixels = 255.0 * image.cpu().numpy()
|
||||
img = Image.fromarray(np.clip(normalized_pixels, 0, 255).astype(np.uint8))
|
||||
|
||||
# 生成パラメータ整理
|
||||
paramsExtractor = self.create_generate_params(img, params, d2_pipe)
|
||||
# 必要な生成パラメーターをまとめたもの
|
||||
gen_info = paramsExtractor.gen_info
|
||||
# EagleやPNGInfo用に整形したもの
|
||||
formated_info = paramsExtractor.format_info(params["memo_text"])
|
||||
|
||||
# print("generate_params", gen_info)
|
||||
# print("format_info", formated_info)
|
||||
|
||||
# 画像をローカルに保存
|
||||
file_name, file_full_path = self.save_image(img, params)
|
||||
file_name, file_full_path = self.save_image(img, params, gen_info, formated_info)
|
||||
|
||||
# Eagleフォルダが指定されているならフォルダIDを取得
|
||||
folder_id = self.eagle_api.find_or_create_folder(params["eagle_folder"])
|
||||
|
||||
# Send image to Eagle
|
||||
item = {"path": file_full_path, "name": file_name}
|
||||
item["annotation"] = util.make_annotation_text(
|
||||
params["positive"], params["negative"], params["memo_text"]
|
||||
)
|
||||
# Eagleに送る情報を作成
|
||||
item = {
|
||||
"path": file_full_path,
|
||||
"name": file_name,
|
||||
"annotation": formated_info,
|
||||
"tags": [],
|
||||
}
|
||||
|
||||
if(params["save_tags"]):
|
||||
item["tags"] = util.get_prompt_tags(params["positive"])
|
||||
# タグを取得
|
||||
item["tags"] = self.get_tags(params, gen_info)
|
||||
|
||||
_ret = self.eagle_api.add_item_from_path(data=item, folder_id=folder_id)
|
||||
|
||||
@@ -162,32 +192,58 @@ class D2_SendEagle:
|
||||
"filename": file_name, "subfolder": self.subfolder_name, "type": self.type
|
||||
}
|
||||
|
||||
# ######################
|
||||
# 登録タグを取得
|
||||
def get_tags(self, params:TNodeParams, gen_info:TGenInfo) -> list:
|
||||
if(params["save_tags"] == "Prompt + Checkpoint"):
|
||||
return [*util.get_prompt_tags(gen_info["positive"]), gen_info["model_name"]]
|
||||
|
||||
elif(params["save_tags"] == "Prompt"):
|
||||
return util.get_prompt_tags(gen_info["positive"])
|
||||
|
||||
elif(params["save_tags"] == "Checkpoint"):
|
||||
return [gen_info["model_name"]]
|
||||
|
||||
return []
|
||||
|
||||
|
||||
# ######################
|
||||
# 画像をローカルに保存
|
||||
def save_image(self, img, params):
|
||||
def save_image(self, img, params:TNodeParams, gen_info:TGenInfo, formated_info:str):
|
||||
file_name = ""
|
||||
file_full_path = ""
|
||||
filename_params = self.get_filename_params(img, params)
|
||||
|
||||
if params["format"] == "webp":
|
||||
# Save webp image file
|
||||
file_name = self.get_filename(params["filename_template"], 'webp', filename_params)
|
||||
file_name = self.get_filename(params["filename_template"], 'webp', gen_info)
|
||||
file_full_path = os.path.join(self.output_folder, file_name)
|
||||
|
||||
exif_data = util.get_exif_from_prompt(
|
||||
img.getexif(), params["prompt"], params["extra_pnginfo"]
|
||||
)
|
||||
exif = util.get_exif_from_prompt(img, formated_info, params["extra_pnginfo"], params["prompt"])
|
||||
|
||||
img.save(
|
||||
file_full_path,
|
||||
quality = params["compression"],
|
||||
exif = exif_data,
|
||||
exif = exif,
|
||||
lossless = params["lossless_webp"],
|
||||
)
|
||||
|
||||
elif params["format"] == "jpeg":
|
||||
# Save jpeg image file
|
||||
file_name = self.get_filename(params["filename_template"], 'jpeg', gen_info)
|
||||
file_full_path = os.path.join(self.output_folder, file_name)
|
||||
|
||||
exif = util.get_exif_from_prompt(img, formated_info, params["extra_pnginfo"], params["prompt"])
|
||||
|
||||
img.save(
|
||||
file_full_path,
|
||||
quality = params["compression"],
|
||||
optimize = True,
|
||||
exif = exif,
|
||||
)
|
||||
|
||||
else:
|
||||
# Save png image file
|
||||
file_name = self.get_filename(params["filename_template"], 'png', filename_params)
|
||||
file_name = self.get_filename(params["filename_template"], 'png', gen_info)
|
||||
file_full_path = os.path.join(self.output_folder, file_name)
|
||||
|
||||
metadata = PngInfo()
|
||||
@@ -202,6 +258,7 @@ class D2_SendEagle:
|
||||
|
||||
return file_name, file_full_path
|
||||
|
||||
|
||||
# ######################
|
||||
# 画像保存パスを取得
|
||||
def get_output_folder(self):
|
||||
@@ -217,40 +274,41 @@ class D2_SendEagle:
|
||||
|
||||
|
||||
# ######################
|
||||
# ファイルネーム用パラメーターを取得
|
||||
def get_filename_params(self, img, params) -> dict:
|
||||
width, height = img.size
|
||||
gen_data = PromptInfoExtractor(params["prompt"])
|
||||
# 生成パラメーターを取得
|
||||
def create_generate_params(self, img, params:TNodeParams, d2_pipe:D2_TD2Pipe | None) -> ParamsExtractor:
|
||||
# print("[SendEagle] create_generate_params - ", params )
|
||||
paramsExtractor = ParamsExtractor(params)
|
||||
paramsExtractor.gen_info["width"] = img.width
|
||||
paramsExtractor.gen_info["height"] = img.height
|
||||
|
||||
|
||||
model = ""
|
||||
steps = ""
|
||||
seed = ""
|
||||
# pipe が指定されていればその値を入力する
|
||||
if d2_pipe != None:
|
||||
if d2_pipe.steps != None:
|
||||
paramsExtractor.gen_info["steps"] = d2_pipe.steps
|
||||
if d2_pipe.sampler_name:
|
||||
paramsExtractor.gen_info["sampler_name"] = d2_pipe.sampler_name
|
||||
if d2_pipe.scheduler:
|
||||
paramsExtractor.gen_info["scheduler"] = d2_pipe.scheduler
|
||||
if d2_pipe.cfg != None:
|
||||
paramsExtractor.gen_info["cfg"] = d2_pipe.cfg
|
||||
if d2_pipe.seed != None:
|
||||
paramsExtractor.gen_info["seed"] = d2_pipe.seed
|
||||
if d2_pipe.ckpt_name:
|
||||
paramsExtractor.gen_info["model_name"] = d2_pipe.ckpt_name.replace("\\", "__")
|
||||
|
||||
if gen_data and hasattr(gen_data, 'info') and gen_data.info:
|
||||
model = os.path.splitext(gen_data.info.get("model_name", ""))[0]
|
||||
steps = gen_data.info.get("steps", "")
|
||||
seed = gen_data.info.get("seed", "")
|
||||
|
||||
return {
|
||||
"width": width,
|
||||
"height": height,
|
||||
"model": model,
|
||||
"steps": steps,
|
||||
"seed": seed,
|
||||
}
|
||||
return paramsExtractor
|
||||
|
||||
|
||||
# ######################
|
||||
# ファイルネームを取得
|
||||
def get_filename(self, template:str, ext:str, filename_params) -> str:
|
||||
def get_filename(self, template:str, ext:str, gen_info:TGenInfo) -> str:
|
||||
base = template.format(
|
||||
width = filename_params["width"],
|
||||
height = filename_params["height"],
|
||||
model = filename_params["model"],
|
||||
steps = filename_params["steps"],
|
||||
seed = filename_params["seed"],
|
||||
width = gen_info["width"],
|
||||
height = gen_info["height"],
|
||||
model = gen_info["model_name"],
|
||||
steps = gen_info["steps"],
|
||||
seed = gen_info["seed"],
|
||||
)
|
||||
|
||||
return f"{util.get_datetime_str_msec()}-{base}.{ext}"
|
||||
|
||||
|
||||
|
||||
@@ -1,59 +1,156 @@
|
||||
# D2 Send Eagle
|
||||
|
||||
ComfyUIで生成した画像を画像管理ソフト [Eagle](https://en.eagle.cool/) に送るカスタムノードです。
|
||||
[English][<a href="README_ja.md">日本語</a>][<a href="README_zh.md">繁体中文</a>]
|
||||
|
||||
|
||||
A custom node for sending images generated in ComfyUI to the image management software [Eagle](https://en.eagle.cool/).
|
||||
|
||||
Please also try [D2 Nodes ComfyUI](https://github.com/da2el-ai/d2-nodes-ComfyUI/), which contains a collection of useful custom nodes.
|
||||
|
||||
[ComfyUI-send-eagle-slim](https://github.com/shingo1228/ComfyUI-send-eagle-slim) という既存のカスタムノードを自分の好みに拡張したものです。素晴らしいカスタムノードを制作してくださった Shingo.T 様に感謝です。
|
||||
|
||||
<img src="img/image.png">
|
||||
|
||||
|
||||
## サンプルワークフロー
|
||||
## Sample Workflows
|
||||
|
||||
<img src="img/sample_workflow.png">
|
||||
### Basic Configuration Workflow
|
||||
|
||||
<a href="workflow/workflow_basic.png" target="_blkank">
|
||||
<img src="workflow/workflow_basic.png">
|
||||
</a>
|
||||
|
||||
### Workflow Using D2 Nodes ComfyUI
|
||||
|
||||
<a href="workflow/workflow_d2-node.png" target="_blkank">
|
||||
<img src="workflow/workflow_d2-node.png">
|
||||
</a>
|
||||
|
||||
- Gets checkpoint name from `D2 Checkpoint Loader` and removes the ".safetensors" extension using `D2 Filename Template`
|
||||
- Retrieves generation parameters from `d2_pipe`
|
||||
|
||||
|
||||
## 主な機能
|
||||
---
|
||||
|
||||
- `image` で受け取った画像を Eagle に送信する
|
||||
- `positive`、`negative` で受け取ったテキストを Eagle のメモとして記録する
|
||||
- 画像の上の入力欄に記入したテキストも Eagle のメモとして記録する
|
||||
- `format`: 保存形式を Webp / PNG から選択
|
||||
- `lossless_webp`: Webp は可逆(lossless)か不可逆(lossy)から選択
|
||||
- `compression`: lossyの時の圧縮率を指定
|
||||
- `save_tags`: プロンプトを Eagle のタグとして保存するか選択
|
||||
- `filename_template`: 保存ファイル名のテンプレート。`{}` 内に該当パラメータが入る
|
||||
- 初期設定:`{model}-{width}-{height}-{seed}`
|
||||
- `{model}`: checkpoint名
|
||||
- `{width}` `{height}`: 画像の幅、高さ
|
||||
- `{steps}`: 生成ステップ数
|
||||
- `{seed}`: 生成シード
|
||||
- `eagle_folder`: Eagleのフォルダ名、またはフォルダIDを指定。フォルダが存在しなければ新規作成する
|
||||
## Main Features
|
||||
|
||||
- Sends images received through `image` to Eagle
|
||||
- Records text received through `positive` and `negative` as Eagle memos
|
||||
- Supports both png and webp formats
|
||||
|
||||
### Generation Parameters Recorded as Image and Eagle Comments
|
||||
|
||||
- Parameters such as positive, negative, CFG, steps are retrieved from the KSampler connected to D2 Send Eagle
|
||||
- May not be retrievable depending on the workflow
|
||||
- Parameters can also be specified through d2_pipe
|
||||
|
||||
|
||||
## その他の機能
|
||||
#### Supported KSamplers
|
||||
|
||||
Eagleに送信する他に、ローカル環境の下記フォルダにも画像を保存します。
|
||||
このフォルダ名は変更できません。
|
||||
The following KSampler types are supported.
|
||||
You can add more by editing `config.yaml`.
|
||||
|
||||
- KSampler
|
||||
- KSamplerAdvanced
|
||||
- KSampler With Refiner (Fooocus)
|
||||
- BNK_TiledKSampler
|
||||
- KSampler (Efficient)
|
||||
- GenerateNAID
|
||||
|
||||
|
||||
### Differences Between png and webp Formats
|
||||
|
||||
- png format
|
||||
- **Can save** ComfyUI workflows
|
||||
- **Cannot display** in StableDiffusion webui A1111's PNGInfo
|
||||
- webp format
|
||||
- **Cannot save** ComfyUI workflows
|
||||
- **Can display** in StableDiffusion webui A1111's PNGInfo
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### Using ComfyUI Manager
|
||||
|
||||
1. Open ComfyUI Manager
|
||||
2. Click `Custom Nodes Manager`
|
||||
3. Search for `D2 Send Eagle`
|
||||
4. Click `Install`
|
||||
5. Restart ComfyUI
|
||||
|
||||
### Using Command Prompt
|
||||
|
||||
1. Open Command Prompt
|
||||
1. Navigate to `{ComfyUI installation folder}/custom_nodes`
|
||||
2. `git clone https://github.com/da2el-ai/ComfyUI-d2-send-eagle`
|
||||
|
||||
---
|
||||
|
||||
## Inputs / Options
|
||||
|
||||
- `images`
|
||||
- Images to save
|
||||
- `d2_pipe`
|
||||
- Collection of generation parameters
|
||||
- `positive`
|
||||
- Positive prompt
|
||||
- `negative`
|
||||
- Negative prompt
|
||||
- `format`
|
||||
- Choose from webp / png / jpeg formats
|
||||
- `lossless_webp`
|
||||
- Choose between lossless and lossy. Active when `webp` is selected.
|
||||
- `compression`
|
||||
- Specify compression rate. Active when selecting `lossy` for webp or when using `jpeg`.
|
||||
- `save_tags`
|
||||
- Choose whether to save as Eagle tags
|
||||
- `None`: Don't save
|
||||
- `Prompt + Checkpoint`: Save prompts and model name
|
||||
- `Prompt`: Save prompts
|
||||
- `Checkpoint`: Save model name
|
||||
- `filename_template`
|
||||
- Specify filename format
|
||||
- Default is `{model}-{width}-{height}-{seed}`
|
||||
- Available parameters: `width`, `height`, `model_name`, `steps`, `seed`
|
||||
- `eagle_folder`
|
||||
- Specify Eagle folder name or folder ID. Creates new folder if it doesn't exist
|
||||
|
||||
---
|
||||
|
||||
## Other Features
|
||||
|
||||
### Local Image Saving
|
||||
|
||||
In addition to sending to Eagle, images are also saved locally in the following folder.
|
||||
This folder name cannot be changed.
|
||||
|
||||
`./ComfyUI/output/YYYY-MM-DD/YYYYMMDD_HHMMss_SSSSSS-{FinalImage_width}-{FinalImage_height}.webp`
|
||||
|
||||
|
||||
## インストール
|
||||
1. Navigaate to ComfyUI custom nodes directory
|
||||
2. `git clone https://github.com/shingo1228/ComfyUI-send-eagle`
|
||||
---
|
||||
|
||||
## Change Log
|
||||
|
||||
## 制限事項
|
||||
PNGInfo の保存は下記のノードに対応しています。それ以外のノードでは情報を取得できません。
|
||||
- 2024/12/28
|
||||
- Added support for d2_pipe in d2-node-comfyui
|
||||
- 2024/11/13
|
||||
- Added JPEG format support
|
||||
- 2024/10/04
|
||||
- Added toggle button to hide image preview
|
||||
- Because enlarging preview also enlarged `memo_text` which was inconvenient
|
||||
- Added IMAGE to output
|
||||
- Just a passthrough. Not necessary but added anyway
|
||||
- 2024/09/29
|
||||
- Added support for NovelAI generation node GenerateNAID
|
||||
- Made settings configurable through `config.yaml`
|
||||
- Added Exif prompt information recording for webp format
|
||||
- 2024/08/19
|
||||
- Fixed issue with model name not being retrieved when using Unet Loader
|
||||
- 2024/08/04
|
||||
- Initial release
|
||||
|
||||
- Sampler:`KSampler`,`KSamplerAdvanced`
|
||||
- Latentイメージ:`Empty Latent Image`
|
||||
- CLIP:`ClIP TextEncoder(Prompt)`,`CLIPTextEncodeSDXL`
|
||||
- Latentイメージ: [ComfyUI-SDXL-EmptyLatentImage](https://github.com/shingo1228/ComfyUI-SDXL-EmptyLatentImage)
|
||||
- Sampler: `KSampler With Refiner (Fooocus)`
|
||||
- Sampler: [BNK_TiledKSampler](https://github.com/BlenderNeko/ComfyUI_TiledKSampler)
|
||||
- Prompt: `SDXL Prompt Styler`
|
||||
---
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
## 変更履歴
|
||||
- 2024/08/04 とりあえず公開
|
||||
D2 Send Eagle is an extension of an existing custom node [ComfyUI-send-eagle-slim](https://github.com/shingo1228/ComfyUI-send-eagle-slim), modified to my preferences. Thanks to Shingo.T for creating such an excellent custom node.
|
||||
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
# D2 Send Eagle
|
||||
|
||||
[<a href="README.md">English</a>][日本語][<a href="README_zh.md">繁体中文</a>]
|
||||
|
||||
ComfyUIで生成した画像を画像管理ソフト [Eagle](https://en.eagle.cool/) に送るカスタムノードです。
|
||||
|
||||
便利なカスタムノードをまとめた [D2 Nodes ComfyUI](https://github.com/da2el-ai/d2-nodes-ComfyUI/)も是非お試しください。
|
||||
|
||||
|
||||
<img src="img/image.png">
|
||||
|
||||
|
||||
## サンプルワークフロー
|
||||
|
||||
### 基本的な構成のワークフロー
|
||||
|
||||
<a href="workflow/workflow_basic.png" target="_blkank">
|
||||
<img src="workflow/workflow_basic.png">
|
||||
</a>
|
||||
|
||||
### D2 Nodes ComfyUI を使ったワークフロー
|
||||
|
||||
<a href="workflow/workflow_d2-node.png" target="_blkank">
|
||||
<img src="workflow/workflow_d2-node.png">
|
||||
</a>
|
||||
|
||||
- チェックポイント名を `D2 Checkpoint Loader` から取得し、`D2 Filename Template` を使って拡張子「.safetensors」を削除している
|
||||
- `d2_pipe` から生成パラメータ—を取得している
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 主な機能
|
||||
|
||||
- `image` で受け取った画像を Eagle に送信する
|
||||
- `positive`、`negative` で受け取ったテキストを Eagle のメモとして記録する
|
||||
- png形式、webp形式が選択可能
|
||||
|
||||
### 画像、Eagleのコメントとして記録する生成パラメーター
|
||||
|
||||
- positive、negtive、CFG、steps などのパラメーターは D2 Send Eagle に接続している KSampler から取得しています
|
||||
- ワークフローによっては取得できないことがあります
|
||||
- d2_pipe からパラメータを指定することもできます
|
||||
|
||||
|
||||
#### 対応 KSampler
|
||||
|
||||
下記の KSampler系に対応しています。
|
||||
`config.yaml` を編集すると自分で増やすことができます。
|
||||
|
||||
- KSampler
|
||||
- KSamplerAdvanced
|
||||
- KSampler With Refiner (Fooocus)
|
||||
- BNK_TiledKSampler
|
||||
- KSampler (Efficient)
|
||||
- GenerateNAID
|
||||
|
||||
|
||||
### png形式と webp形式の違い
|
||||
|
||||
- png形式
|
||||
- ComfyUIワークフローを**保存できる**
|
||||
- StableDiffusion webui A1111 の PNGInfoには**表示できない**
|
||||
- webp形式
|
||||
- ComfyUIワークフローを**保存できない**
|
||||
- StableDiffusion webui A1111 の PNGInfoには**表示できる**
|
||||
|
||||
---
|
||||
|
||||
## インストール
|
||||
|
||||
### ComfyUI Manager を使う場合
|
||||
|
||||
1. ComfyUI Manager を開く
|
||||
2. `Custom Nodes Manager` をクリック
|
||||
3. `D2 Send Eagle` を検索
|
||||
4. `Install` をクリック
|
||||
5. ComfyUI を再起動
|
||||
|
||||
### コマンドプロンプトを使う場合
|
||||
|
||||
1. コマンドプロンプトを開く
|
||||
1. `{ComfyUIインストールフォルダ}/custom_nodes` に移動
|
||||
2. `git clone https://github.com/da2el-ai/ComfyUI-d2-send-eagle`
|
||||
|
||||
---
|
||||
|
||||
## 入力 / オプション
|
||||
|
||||
- `images`
|
||||
- 保存する画像
|
||||
- `d2_pipe`
|
||||
- 生成パラメータをまとめたもの
|
||||
- `positive`
|
||||
- ポジティブプロンプト
|
||||
- `negative`
|
||||
- ネガティブプロンプト
|
||||
- `format`
|
||||
- 保存形式を webp / png / jpeg から選択
|
||||
- `lossless_webp`
|
||||
- 可逆(lossless)か不可逆(lossy)から選択。`webp` を選択した時に有効。
|
||||
- `compression`
|
||||
- 圧縮率を指定。webp で `lossy` を選択するか、または `jpeg` を選択したときに有効。
|
||||
- `save_tags`
|
||||
- Eagle のタグとして保存するか選択
|
||||
- `None`: 保存しない
|
||||
- `Prompt + Checkpoint`: プロンプトとモデル名を保存
|
||||
- `Prompt`: プロンプトを保存
|
||||
- `Checkpoint`: モデル名を保存
|
||||
- `filename_template`
|
||||
- ファイル名の書式を指定
|
||||
- 初期設定では `{model}-{width}-{height}-{seed}`
|
||||
- 使用可能なパラメーター: `width`、`height`、`model_name`、`steps`、`seed`
|
||||
- `eagle_folder`
|
||||
- Eagleのフォルダ名、またはフォルダIDを指定。フォルダが存在しなければ新規作成する
|
||||
|
||||
---
|
||||
|
||||
## その他の機能
|
||||
|
||||
### ローカルにも画像を保存
|
||||
|
||||
Eagleに送信する他に、ローカル環境の下記フォルダにも画像を保存します。
|
||||
このフォルダ名は変更できません。
|
||||
|
||||
`./ComfyUI/output/YYYY-MM-DD/YYYYMMDD_HHMMss_SSSSSS-{FinalImage_width}-{FinalImage_height}.webp`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 変更履歴
|
||||
|
||||
- 2024/12/28
|
||||
- d2-node-comfyui の d2_pipe に対応
|
||||
- 2024/11/13
|
||||
- JPEG方式に対応
|
||||
- 2024/10/04
|
||||
- イメージプレビューを非表示にするトグルボタンを追加
|
||||
- プレビューを大きくすると `memo_text` も一緒に大きくなって邪魔だったので
|
||||
- output に IMAGE を追加
|
||||
- ただのパススルーです。必要ではないけど追加しました
|
||||
- 2024/09/29
|
||||
- NovelAI生成ノード GenerateNAID に対応
|
||||
- 設定項目を `config.yaml` で指定できるようにした
|
||||
- webp形式の時にプロンプト情報を Exif に記録するようにした
|
||||
- 2024/08/19
|
||||
- Unet Loaderを使った時にモデル名を取得できないのを修正
|
||||
- 2024/08/04
|
||||
- とりあえず公開
|
||||
|
||||
---
|
||||
|
||||
## 謝辞
|
||||
|
||||
D2 Send Eagle は [ComfyUI-send-eagle-slim](https://github.com/shingo1228/ComfyUI-send-eagle-slim) という既存のカスタムノードを自分の好みに拡張したものです。素晴らしいカスタムノードを制作してくださった Shingo.T 様に感謝です。
|
||||
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
# D2 Send Eagle
|
||||
|
||||
[<a href="README.md">English</a>][<a href="README_ja.md">日本語</a>][繁体中文]
|
||||
|
||||
|
||||
這是一個自定義節點,用於將 ComfyUI 生成的圖片發送到圖片管理軟件 [Eagle](https://en.eagle.cool/)。
|
||||
|
||||
也請嘗試使用 [D2 Nodes ComfyUI](https://github.com/da2el-ai/d2-nodes-ComfyUI/),其中包含了許多實用的自定義節點。
|
||||
|
||||
|
||||
<img src="img/image.png">
|
||||
|
||||
|
||||
## 範例工作流程
|
||||
|
||||
### 基礎配置工作流程
|
||||
|
||||
<a href="workflow/workflow_basic.png" target="_blkank">
|
||||
<img src="workflow/workflow_basic.png">
|
||||
</a>
|
||||
|
||||
### 使用 D2 Nodes ComfyUI 的工作流程
|
||||
|
||||
<a href="workflow/workflow_d2-node.png" target="_blkank">
|
||||
<img src="workflow/workflow_d2-node.png">
|
||||
</a>
|
||||
|
||||
- 從 `D2 Checkpoint Loader` 獲取檢查點名稱,並使用 `D2 Filename Template` 刪除 ".safetensors" 副檔名
|
||||
- 從 `d2_pipe` 獲取生成參數
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 主要功能
|
||||
|
||||
- 將通過 `image` 接收的圖片發送到 Eagle
|
||||
- 將通過 `positive` 和 `negative` 接收的文字記錄為 Eagle 備註
|
||||
- 支持 png 和 webp 格式
|
||||
|
||||
### 作為圖片和 Eagle 註釋記錄的生成參數
|
||||
|
||||
- 參數如 positive、negative、CFG、steps 等是從連接到 D2 Send Eagle 的 KSampler 獲取的
|
||||
- 根據工作流程的不同,可能無法獲取
|
||||
- 也可以通過 d2_pipe 指定參數
|
||||
|
||||
|
||||
#### 支持的 KSampler
|
||||
|
||||
支持以下 KSampler 類型。
|
||||
可以通過編輯 `config.yaml` 來添加更多。
|
||||
|
||||
- KSampler
|
||||
- KSamplerAdvanced
|
||||
- KSampler With Refiner (Fooocus)
|
||||
- BNK_TiledKSampler
|
||||
- KSampler (Efficient)
|
||||
- GenerateNAID
|
||||
|
||||
|
||||
### png 和 webp 格式的區別
|
||||
|
||||
- png 格式
|
||||
- **可以**保存 ComfyUI 工作流程
|
||||
- **無法**顯示在 StableDiffusion webui A1111 的 PNGInfo 中
|
||||
- webp 格式
|
||||
- **無法**保存 ComfyUI 工作流程
|
||||
- **可以**顯示在 StableDiffusion webui A1111 的 PNGInfo 中
|
||||
|
||||
---
|
||||
|
||||
## 安裝
|
||||
|
||||
### 使用 ComfyUI Manager
|
||||
|
||||
1. 打開 ComfyUI Manager
|
||||
2. 點擊 `Custom Nodes Manager`
|
||||
3. 搜索 `D2 Send Eagle`
|
||||
4. 點擊 `Install`
|
||||
5. 重啟 ComfyUI
|
||||
|
||||
### 使用命令提示符
|
||||
|
||||
1. 打開命令提示符
|
||||
1. 進入 `{ComfyUI 安裝資料夾}/custom_nodes`
|
||||
2. `git clone https://github.com/da2el-ai/ComfyUI-d2-send-eagle`
|
||||
|
||||
---
|
||||
|
||||
## 輸入 / 選項
|
||||
|
||||
- `images`
|
||||
- 要保存的圖片
|
||||
- `d2_pipe`
|
||||
- 生成參數的集合
|
||||
- `positive`
|
||||
- 正向提示詞
|
||||
- `negative`
|
||||
- 負向提示詞
|
||||
- `format`
|
||||
- 從 webp / png / jpeg 中選擇保存格式
|
||||
- `lossless_webp`
|
||||
- 選擇無損(lossless)或有損(lossy)。選擇 `webp` 時有效。
|
||||
- `compression`
|
||||
- 指定壓縮率。選擇 webp 的 `lossy` 或使用 `jpeg` 時有效。
|
||||
- `save_tags`
|
||||
- 選擇是否保存為 Eagle 標籤
|
||||
- `None`: 不保存
|
||||
- `Prompt + Checkpoint`: 保存提示詞和模型名稱
|
||||
- `Prompt`: 保存提示詞
|
||||
- `Checkpoint`: 保存模型名稱
|
||||
- `filename_template`
|
||||
- 指定檔案名格式
|
||||
- 預設為 `{model}-{width}-{height}-{seed}`
|
||||
- 可用參數:`width`、`height`、`model_name`、`steps`、`seed`
|
||||
- `eagle_folder`
|
||||
- 指定 Eagle 資料夾名稱或資料夾 ID。如果不存在則創建新資料夾
|
||||
|
||||
---
|
||||
|
||||
## 其他功能
|
||||
|
||||
### 本地圖片保存
|
||||
|
||||
除了發送到 Eagle,圖片也會保存在本地的以下資料夾中。
|
||||
此資料夾名稱無法更改。
|
||||
|
||||
`./ComfyUI/output/YYYY-MM-DD/YYYYMMDD_HHMMss_SSSSSS-{FinalImage_width}-{FinalImage_height}.webp`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 更新日誌
|
||||
|
||||
- 2024/12/28
|
||||
- 添加對 d2-node-comfyui 的 d2_pipe 支持
|
||||
- 2024/11/13
|
||||
- 添加 JPEG 格式支持
|
||||
- 2024/10/04
|
||||
- 添加隱藏圖片預覽的切換按鈕
|
||||
- 因為放大預覽時 `memo_text` 也會一起放大很不方便
|
||||
- 添加 IMAGE 到輸出
|
||||
- 僅作為傳遞。不是必要的但還是添加了
|
||||
- 2024/09/29
|
||||
- 添加對 NovelAI 生成節點 GenerateNAID 的支持
|
||||
- 使設置可通過 `config.yaml` 配置
|
||||
- 為 webp 格式添加 Exif 提示詞信息記錄
|
||||
- 2024/08/19
|
||||
- 修復使用 Unet Loader 時無法獲取模型名稱的問題
|
||||
- 2024/08/04
|
||||
- 初始發布
|
||||
|
||||
---
|
||||
|
||||
## 致謝
|
||||
|
||||
D2 Send Eagle 是對現有自定義節點 [ComfyUI-send-eagle-slim](https://github.com/shingo1228/ComfyUI-send-eagle-slim) 的擴展,根據個人喜好進行了修改。感謝 Shingo.T 製作了如此優秀的自定義節點。
|
||||
@@ -0,0 +1,11 @@
|
||||
ksamplers:
|
||||
- "D2 KSampler"
|
||||
- "KSampler"
|
||||
- "KSamplerAdvanced"
|
||||
- "KSampler With Refiner (Fooocus)"
|
||||
- "BNK_TiledKSampler"
|
||||
- "KSampler (Efficient)"
|
||||
- "GenerateNAID"
|
||||
|
||||
output_format: "{positive}\n\nNegative prompt:{negative}\nSteps: {steps}, Sampler: {sampler_name} {scheduler}, CFG scale: {cfg}, Seed: {seed}, Size: {width}x{height}, Model: {model_name}\n\nMemo: {memo}"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 429 KiB |
@@ -0,0 +1,207 @@
|
||||
import json
|
||||
import re
|
||||
import yaml
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from ..my_types import TGenInfo, TConfig
|
||||
|
||||
|
||||
DEBUG = False
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
CONFIG_FILE = BASE_DIR / 'config.yaml'
|
||||
CONFIG_ORG = BASE_DIR / 'config.org.yaml'
|
||||
|
||||
|
||||
def dprint(str, dict=[]):
|
||||
if DEBUG:
|
||||
print(f"debug:{str}")
|
||||
if dict:
|
||||
print(json.dumps(dict, indent=2))
|
||||
|
||||
|
||||
class ParamsExtractor:
|
||||
def __init__(self, params, ):
|
||||
"""constructor
|
||||
|
||||
Args:
|
||||
prompt (_type_): ComfyUI hidden object "prompt".
|
||||
"""
|
||||
self._prompt = params["prompt"]
|
||||
|
||||
if DEBUG:
|
||||
self._show_data()
|
||||
|
||||
self._load_config()
|
||||
self.gen_info:TGenInfo = self._gather_info(params["positive"], params["negative"])
|
||||
|
||||
|
||||
# ##################
|
||||
# プロンプトからEagleタグを取得
|
||||
def get_prompt_tags(self):
|
||||
cleaned_string = re.sub(r":\d+\.\d+", "", self.gen_info["positive"])
|
||||
items = cleaned_string.split(",")
|
||||
|
||||
return [
|
||||
re.sub(r"[\(\)]", "", item).strip()
|
||||
for item in items
|
||||
if re.sub(r"[\(\)]", "", item).strip()
|
||||
]
|
||||
|
||||
|
||||
# ##################
|
||||
# 設定ファイルを読み込む
|
||||
def _load_config(self):
|
||||
# ユーザー設定がなければオリジナル設定をコピーする
|
||||
if not os.path.exists(CONFIG_FILE):
|
||||
shutil.copy2(CONFIG_ORG, CONFIG_FILE)
|
||||
|
||||
with open(CONFIG_FILE, "r", encoding="utf-8") as file:
|
||||
self.config:TConfig = yaml.safe_load(file)
|
||||
|
||||
|
||||
# ##################
|
||||
# 受け取ったパラメターを表示
|
||||
def _show_data(self):
|
||||
dprint("prompt(hidden object)", self._prompt)
|
||||
|
||||
|
||||
# ##################
|
||||
# 必要なパラメーターをまとめる
|
||||
def _gather_info(self, positive, negative) -> TGenInfo:
|
||||
|
||||
gen_info:TGenInfo = {
|
||||
"steps": 0,
|
||||
"sampler_name": "",
|
||||
"scheduler": "",
|
||||
"cfg": 0,
|
||||
"seed": 0,
|
||||
"model_name": "",
|
||||
"width": 0,
|
||||
"height": 0,
|
||||
"positive": positive,
|
||||
"negative": negative,
|
||||
}
|
||||
|
||||
ksamplers = self._get_ksamplers()
|
||||
|
||||
if not ksamplers:
|
||||
return gen_info
|
||||
|
||||
# 最初のKsamplerのみ対象とする
|
||||
key, ksampler = ksamplers[0]
|
||||
|
||||
gen_info["model_name"] = self._extract_model_name(ksampler)
|
||||
width, height = self._extract_image_size(ksampler)
|
||||
gen_info["width"] = width
|
||||
gen_info["height"] = height
|
||||
gen_info["steps"] = ksampler["inputs"]["steps"]
|
||||
gen_info["sampler_name"] = self._extract_sampler_name(ksampler)
|
||||
gen_info["scheduler"] = ksampler["inputs"]["scheduler"]
|
||||
gen_info["cfg"] = ksampler["inputs"]["cfg"]
|
||||
gen_info["seed"] = ksampler["inputs"].get(
|
||||
"seed", ksampler["inputs"].get("noise_seed", None)
|
||||
)
|
||||
|
||||
return gen_info
|
||||
|
||||
|
||||
def _get_ksamplers(self):
|
||||
ksamplers = [
|
||||
(k, v)
|
||||
for k, v in self._prompt.items()
|
||||
if v["class_type"] in self.config["ksamplers"]
|
||||
]
|
||||
return sorted(ksamplers, key=lambda x: int(x[0]))
|
||||
|
||||
|
||||
# ######################
|
||||
# モデル名を取得
|
||||
def _extract_model_name(self, node):
|
||||
try:
|
||||
model_input = node.get("inputs", {}).get("model")
|
||||
if model_input and isinstance(model_input, list) and model_input:
|
||||
ckpt_name = self._get_ckpt_name(model_input[0])
|
||||
if ckpt_name:
|
||||
return ckpt_name.replace("/", "_").replace("\\", "_")
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
|
||||
# モデル名読み込んだノードIDからモデル名を取得
|
||||
def _get_ckpt_name(self, node_number):
|
||||
"""Recursively search for the 'ckpt_name' key starting from the specified node."""
|
||||
node = self._prompt[node_number]
|
||||
if "ckpt_name" in node["inputs"]:
|
||||
return node["inputs"]["ckpt_name"]
|
||||
if "model" in node["inputs"]:
|
||||
return self._get_ckpt_name(node["inputs"]["model"][0])
|
||||
if "unet_name" in node["inputs"]:
|
||||
return node["inputs"]["unet_name"]
|
||||
return None
|
||||
|
||||
|
||||
# ######################
|
||||
# サンプラー名を取得
|
||||
def _extract_sampler_name(self, node):
|
||||
sampler = node["inputs"].get("sampler") or node["inputs"].get("sampler_name")
|
||||
return sampler if sampler is not None else ""
|
||||
|
||||
# ######################
|
||||
# 画像サイズを取得
|
||||
def _extract_image_size(self, node):
|
||||
# サイズ情報を持ってるならそのまま返す
|
||||
if "width" in node["inputs"]:
|
||||
return node["inputs"]["width"], node["inputs"]["height"]
|
||||
|
||||
width, height = self._get_image_size_from_latent_image(node["inputs"]["latent_image"][0])
|
||||
return width, height
|
||||
|
||||
def _get_image_size_from_latent_image(self, node_number):
|
||||
target_node = self._prompt[node_number]
|
||||
|
||||
if "width" in target_node["inputs"]:
|
||||
return target_node["inputs"]["width"], target_node["inputs"]["height"]
|
||||
|
||||
if "clip_width" in target_node.get("outputs", {}):
|
||||
return target_node["outputs"]["clip_width"], target_node["outputs"]["clip_height"]
|
||||
|
||||
elif target_node["class_type"] == "SDXL Empty Latent Image":
|
||||
resolusion_str = target_node["inputs"]["resolution"]
|
||||
pattern = r"(\d+) x (\d+)"
|
||||
match = re.search(pattern, resolusion_str)
|
||||
return int(match.group(1)), int(match.group(2))
|
||||
|
||||
else:
|
||||
return 0, 0
|
||||
|
||||
# ######################
|
||||
# PngInfoやEagleメモ用のテキストに整形
|
||||
def format_info(self, memo:str):
|
||||
formatted_str = self.config["output_format"].format(**self.gen_info, memo=memo)
|
||||
return formatted_str
|
||||
|
||||
# def extract_and_format(self):
|
||||
# """Extract and format the required information from the loaded JSON data."""
|
||||
# info = self._gather_info()
|
||||
# if not info:
|
||||
# return "No suitable data found."
|
||||
# return self.format_info(info)
|
||||
|
||||
# def formatted_annotation(self):
|
||||
# annotation = ""
|
||||
# if len(self.info["prompt"]) > 0:
|
||||
# annotation += self.info["prompt"]
|
||||
|
||||
# if len(self.info["negative"]) > 0:
|
||||
# if len(annotation) > 0:
|
||||
# annotation += "\n"
|
||||
# annotation += "Negative prompt: "
|
||||
# annotation += self.info["negative"]
|
||||
|
||||
# if len(annotation) > 0:
|
||||
# annotation += "\n"
|
||||
# annotation += self.extract_and_format()
|
||||
|
||||
# return annotation
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
import json
|
||||
import re
|
||||
|
||||
DEBUG = False
|
||||
|
||||
|
||||
def dprint(str):
|
||||
if DEBUG:
|
||||
print(f"debug:{str}")
|
||||
|
||||
|
||||
class PromptInfoExtractor:
|
||||
def __init__(self, prompt, config_filepath=None):
|
||||
"""constructor
|
||||
|
||||
Args:
|
||||
prompt (_type_): ComfyUI hidden object "prompt".
|
||||
config_filepath (_type_, optional): Configration file path. Defaults to None.
|
||||
"""
|
||||
# self.load_data(prompt)
|
||||
self._prompt = prompt
|
||||
|
||||
if DEBUG:
|
||||
self._show_data()
|
||||
|
||||
if config_filepath:
|
||||
self.load_config(config_filepath)
|
||||
else:
|
||||
self.config = {
|
||||
"search_class_types": ["KSampler", "KSamplerAdvanced", "KSampler With Refiner (Fooocus)", "BNK_TiledKSampler"],
|
||||
"output_format": "Steps: {steps}, Sampler: {sampler_name} {scheduler}, CFG scale: {cfg}, Seed: {seed}, Size: {width}x{height}, Model: {model_name}",
|
||||
}
|
||||
self.info = self.gather_info()
|
||||
|
||||
def load_data(self, json_filepath):
|
||||
"""Load JSON data from the provided filepath."""
|
||||
with open(json_filepath, "r") as file:
|
||||
self._prompt = json.load(file)
|
||||
|
||||
def load_config(self, config_filepath):
|
||||
"""Load configuration from the provided filepath."""
|
||||
with open(config_filepath, "r") as config_file:
|
||||
self.config = json.load(config_file)
|
||||
|
||||
def _show_data(self):
|
||||
"""For debug. show information of ComfyUI hidden object "prompt" and "extra_onginfo"."""
|
||||
dprint(f"prompt(hidden object){self._prompt}")
|
||||
|
||||
def gather_info(self):
|
||||
ksampler_items = self.get_ksampler_items()
|
||||
|
||||
if not ksampler_items:
|
||||
return None
|
||||
|
||||
key, ksampler_item = ksampler_items[0]
|
||||
|
||||
model_name = self.extract_model_name(ksampler_item)
|
||||
latent_image_info = self.extract_latent_image_info(ksampler_item)
|
||||
prompt_text = self.extract_prompt_info()
|
||||
|
||||
info_dict = {
|
||||
"steps": ksampler_item["inputs"]["steps"],
|
||||
"sampler_name": ksampler_item["inputs"]["sampler_name"],
|
||||
"scheduler": ksampler_item["inputs"]["scheduler"],
|
||||
"cfg": ksampler_item["inputs"]["cfg"],
|
||||
"seed": ksampler_item["inputs"].get(
|
||||
"seed", ksampler_item["inputs"].get("noise_seed", None)
|
||||
),
|
||||
"model_name": model_name,
|
||||
"width": latent_image_info["inputs"]["width"],
|
||||
"height": latent_image_info["inputs"]["height"],
|
||||
}
|
||||
if "prompt" in prompt_text:
|
||||
info_dict["prompt"] = prompt_text["prompt"]
|
||||
else:
|
||||
info_dict["prompt"] = ""
|
||||
|
||||
if "negative" in prompt_text:
|
||||
info_dict["negative"] = prompt_text["negative"]
|
||||
else:
|
||||
info_dict["negative"] = ""
|
||||
|
||||
return info_dict
|
||||
|
||||
def get_ksampler_items(self):
|
||||
ksampler_items = [
|
||||
(k, v)
|
||||
for k, v in self._prompt.items()
|
||||
if v["class_type"] in self.config["search_class_types"]
|
||||
]
|
||||
return sorted(ksampler_items, key=lambda x: int(x[0]))
|
||||
|
||||
def extract_model_name(self, item):
|
||||
return (
|
||||
self.get_ckpt_name(item["inputs"]["model"][0])
|
||||
.replace("/", "_")
|
||||
.replace("\\", "_")
|
||||
)
|
||||
|
||||
def get_ckpt_name(self, node_number):
|
||||
"""Recursively search for the 'ckpt_name' key starting from the specified node."""
|
||||
node = self._prompt[node_number]
|
||||
if "ckpt_name" in node["inputs"]:
|
||||
return node["inputs"]["ckpt_name"]
|
||||
if "model" in node["inputs"]:
|
||||
return self.get_ckpt_name(node["inputs"]["model"][0])
|
||||
return None
|
||||
|
||||
def extract_latent_image_info(self, item):
|
||||
latent_image_node_number = item["inputs"]["latent_image"][0]
|
||||
target_item = self._prompt[latent_image_node_number]
|
||||
|
||||
if target_item["class_type"] == "EmptyLatentImage":
|
||||
return target_item
|
||||
|
||||
elif target_item["class_type"] == "SDXL Empty Latent Image":
|
||||
resolusion_str = target_item["inputs"]["resolution"]
|
||||
pattern = r"(\d+) x (\d+)"
|
||||
match = re.search(pattern, resolusion_str)
|
||||
|
||||
latent_image_info = {
|
||||
"inputs": {"width": int(match.group(1)), "height": int(match.group(2))}
|
||||
}
|
||||
return latent_image_info
|
||||
|
||||
else:
|
||||
latent_image_info = {"inputs": {"width": 0, "height": 0}}
|
||||
return latent_image_info
|
||||
|
||||
def extract_prompt_info(self):
|
||||
positive_text = self.extract_text_by_key("positive")
|
||||
negative_text = self.extract_text_by_key("negative")
|
||||
|
||||
info_dict = {}
|
||||
if positive_text:
|
||||
info_dict["prompt"] = positive_text
|
||||
if negative_text:
|
||||
info_dict["negative"] = negative_text
|
||||
|
||||
return info_dict
|
||||
|
||||
def extract_text_by_key(self, key):
|
||||
"""Extract text by the given key, either 'positive' or 'negative'."""
|
||||
ksampler_items = self.get_ksampler_items()
|
||||
|
||||
if not ksampler_items:
|
||||
return None
|
||||
|
||||
ksampler_item = ksampler_items[0][1]
|
||||
|
||||
if key not in ksampler_item["inputs"]:
|
||||
return None
|
||||
|
||||
target_node_number = ksampler_item["inputs"][key][0]
|
||||
target_node = self._prompt.get(str(target_node_number), {})
|
||||
extracted_text = self.extract_text_from_node_v2(target_node, key)
|
||||
|
||||
return extracted_text
|
||||
|
||||
def extract_text_from_node_v2(self, node, key):
|
||||
# For CLIPTextEncoder
|
||||
direct_text = node.get("inputs", {}).get("text")
|
||||
if isinstance(direct_text, str):
|
||||
return direct_text
|
||||
|
||||
# For SDXLCLIPTextEncoder
|
||||
text_g = node.get("inputs", {}).get("text_g")
|
||||
text_l = node.get("inputs", {}).get("text_l")
|
||||
if text_g and text_l:
|
||||
if text_g == text_l:
|
||||
return text_g
|
||||
else:
|
||||
return f"text_g:{text_g} text_l:{text_l}"
|
||||
elif text_g:
|
||||
return text_g
|
||||
elif text_l:
|
||||
return text_l
|
||||
|
||||
# For SDXLPromptStyler
|
||||
try:
|
||||
node_number = node.get("inputs", {}).get("text")[0]
|
||||
if key == "positive":
|
||||
text_positive = self._prompt[node_number]["inputs"]["text_positive"]
|
||||
if text_positive:
|
||||
return text_positive
|
||||
else:
|
||||
text_negative = self._prompt[node_number]["inputs"]["text_negative"]
|
||||
if text_negative:
|
||||
return text_negative
|
||||
except(Exception) as e:
|
||||
return None
|
||||
|
||||
if DEBUG:
|
||||
return "Prompt parse error"
|
||||
else:
|
||||
return None
|
||||
|
||||
def format_info(self, info_dict):
|
||||
"""Format the gathered information based on the configuration."""
|
||||
formatted_str = self.config["output_format"].format(**info_dict)
|
||||
return formatted_str
|
||||
|
||||
def extract_and_format(self):
|
||||
"""Extract and format the required information from the loaded JSON data."""
|
||||
info = self.gather_info()
|
||||
if not info:
|
||||
return "No suitable data found."
|
||||
return self.format_info(info)
|
||||
|
||||
def formatted_annotation(self):
|
||||
annotation = ""
|
||||
if len(self.info["prompt"]) > 0:
|
||||
annotation += self.info["prompt"]
|
||||
|
||||
if len(self.info["negative"]) > 0:
|
||||
if len(annotation) > 0:
|
||||
annotation += "\n"
|
||||
annotation += "Negative prompt: "
|
||||
annotation += self.info["negative"]
|
||||
|
||||
if len(annotation) > 0:
|
||||
annotation += "\n"
|
||||
annotation += self.extract_and_format()
|
||||
|
||||
return annotation
|
||||
|
||||
def get_prompt_tags(self):
|
||||
cleaned_string = re.sub(r":\d+\.\d+", "", self.info["prompt"])
|
||||
items = cleaned_string.split(",")
|
||||
|
||||
return [
|
||||
re.sub(r"[\(\)]", "", item).strip()
|
||||
for item in items
|
||||
if re.sub(r"[\(\)]", "", item).strip()
|
||||
]
|
||||
+59
-52
@@ -3,84 +3,91 @@ import json
|
||||
import traceback
|
||||
import re
|
||||
from PIL import Image
|
||||
import piexif
|
||||
import piexif.helper
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional, TypedDict, Union, List
|
||||
from ..my_types import TGenInfo
|
||||
|
||||
|
||||
|
||||
class util:
|
||||
@staticmethod
|
||||
def initialize_defaults(prompt, extra_pnginfo):
|
||||
util.write_prompt(prompt, extra_pnginfo)
|
||||
print("check prompt_decode_err.log")
|
||||
traceback.print_exc()
|
||||
return "", [], "unknown", "00", "000000"
|
||||
# @staticmethod
|
||||
# def initialize_defaults(prompt, extra_pnginfo):
|
||||
# util.write_prompt(prompt, extra_pnginfo)
|
||||
# print("check prompt_decode_err.log")
|
||||
# traceback.print_exc()
|
||||
# return "", [], "unknown", "00", "000000"
|
||||
|
||||
# @staticmethod
|
||||
# def write_prompt(prompt, extra_pnginfo):
|
||||
# log_file_name = os.path.join(os.path.dirname(__file__), "prompt_decode_err.log")
|
||||
# with open(log_file_name, "w", encoding="utf-8") as f:
|
||||
# f.write('"prompt:"\n')
|
||||
# json.dump(prompt, f, indent=4, ensure_ascii=False)
|
||||
# if extra_pnginfo is not None:
|
||||
# f.write('\n\n"extra_pnginfo:"\n')
|
||||
# json.dump(extra_pnginfo, f, indent=4, ensure_ascii=False)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def write_prompt(prompt, extra_pnginfo):
|
||||
log_file_name = os.path.join(os.path.dirname(__file__), "prompt_decode_err.log")
|
||||
with open(log_file_name, "w", encoding="utf-8") as f:
|
||||
f.write('"prompt:"\n')
|
||||
json.dump(prompt, f, indent=4, ensure_ascii=False)
|
||||
if extra_pnginfo is not None:
|
||||
f.write('\n\n"extra_pnginfo:"\n')
|
||||
json.dump(extra_pnginfo, f, indent=4, ensure_ascii=False)
|
||||
def _is_valid_text(text):
|
||||
return isinstance(text, str) and text.strip() and text != "undefined"
|
||||
|
||||
@staticmethod
|
||||
def make_annotation_text(
|
||||
prompt_text: str, negative_text: str, memo_text: str
|
||||
) -> str:
|
||||
"""
|
||||
Generates annotation text.
|
||||
# # #############################
|
||||
# # Eagle保存用コメントを作成
|
||||
# @staticmethod
|
||||
# def make_annotation_text(gen_info:TGenInfo) -> str:
|
||||
# positive = gen_info["prompt_text"]["prompt"]
|
||||
# negative = gen_info["prompt_text"]["negative"]
|
||||
# tmp_annotation = ""
|
||||
|
||||
This method creates text for annotations by combining prompt text, negative text, and memo text.
|
||||
"""
|
||||
# if util._is_valid_text(positive):
|
||||
# tmp_annotation += positive
|
||||
# if util._is_valid_text(negative):
|
||||
# tmp_annotation += "\n" if tmp_annotation.strip() else ""
|
||||
# tmp_annotation += "Negative prompt:" + negative
|
||||
# if util._is_valid_text(memo_text):
|
||||
# tmp_annotation += "\n" if tmp_annotation.strip() else ""
|
||||
# tmp_annotation += "Memo:" + memo_text
|
||||
# return tmp_annotation
|
||||
|
||||
def is_valid_text(text):
|
||||
return isinstance(text, str) and text.strip() and text != "undefined"
|
||||
|
||||
tmp_annotation = ""
|
||||
if is_valid_text(prompt_text):
|
||||
tmp_annotation += prompt_text
|
||||
if is_valid_text(negative_text):
|
||||
tmp_annotation += "\n" if tmp_annotation.strip() else ""
|
||||
tmp_annotation += "Negative prompt:" + negative_text
|
||||
if is_valid_text(memo_text):
|
||||
tmp_annotation += "\n" if tmp_annotation.strip() else ""
|
||||
tmp_annotation += "Memo:" + memo_text
|
||||
return tmp_annotation
|
||||
|
||||
# ####################
|
||||
# 現在時刻のテキストを取得
|
||||
@staticmethod
|
||||
def get_datetime_str_msec() -> str:
|
||||
"""
|
||||
Gets the current datetime as a string with millisecond precision.
|
||||
|
||||
This method generates and returns a string representing the current datetime with millisecond precision.
|
||||
"""
|
||||
now = datetime.now()
|
||||
date_time_str = now.strftime("%Y%m%d_%H%M%S")
|
||||
return f"{date_time_str}_{now.microsecond:06}"
|
||||
|
||||
|
||||
# #########################
|
||||
# webp用にExifデータを作成する
|
||||
# ただしa1111のPNGInfoに表示するが、ワークフローは再現しない
|
||||
@staticmethod
|
||||
def get_exif_from_prompt(
|
||||
emptyExifData: Image.Exif, prompt, extra_pnginfo
|
||||
) -> Image.Exif:
|
||||
"""
|
||||
Generate exif information for webp format from hidden items "prompt" and "extra_pnginfo
|
||||
"""
|
||||
imgexif = emptyExifData
|
||||
def get_exif_from_prompt(img, formated_info:str, extra_pnginfo, prompt):
|
||||
metadata = img.getexif()
|
||||
|
||||
if len(metadata) == 0:
|
||||
metadata = {"0th": {}, "Exif": {}, "GPS": {}, "Interop": {}, "1st": {}}
|
||||
|
||||
# Add PromptString to EXIF position 0x010f (Exif.Image.Make)
|
||||
if prompt is not None:
|
||||
promptstr = "".join(json.dumps(prompt))
|
||||
imgexif[0x010F] = "Prompt:" + promptstr
|
||||
|
||||
# Add Workflowstring to EXIF position 0x010e (Exif.Image.ImageDescription)
|
||||
metadata["0th"][0x0110] = "prompt:{}".format(json.dumps(prompt))
|
||||
if extra_pnginfo is not None:
|
||||
workflowmetadata = str()
|
||||
inital_exif = 0x010f
|
||||
for x in extra_pnginfo:
|
||||
workflowmetadata += "".join(json.dumps(extra_pnginfo[x]))
|
||||
imgexif[0x010E] = "Workflow:" + workflowmetadata
|
||||
metadata["0th"][inital_exif] = "{}:{}".format(x, json.dumps(extra_pnginfo[x]))
|
||||
inital_exif -= 1
|
||||
|
||||
metadata["Exif"][piexif.ExifIFD.UserComment] = piexif.helper.UserComment.dump(formated_info, "unicode")
|
||||
|
||||
return piexif.dump(metadata)
|
||||
|
||||
return imgexif
|
||||
|
||||
@staticmethod
|
||||
def get_prompt_tags(prompt_text: str) -> list:
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Optional, TypedDict, Union, List
|
||||
|
||||
# SendEagleノードの入力パラメーター
|
||||
class TNodeParams(TypedDict):
|
||||
format: str
|
||||
lossless_webp: bool
|
||||
save_tags: str
|
||||
filename_template: str
|
||||
eagle_folder: str
|
||||
compression: int
|
||||
positive: Optional[str]
|
||||
negative: Optional[str]
|
||||
memo_text: str
|
||||
prompt: Optional[Dict]
|
||||
extra_pnginfo: Optional[Dict]
|
||||
|
||||
|
||||
# 生成情報をまとめた辞書
|
||||
class TGenInfo(TypedDict):
|
||||
steps: int
|
||||
sampler_name: str
|
||||
scheduler: str
|
||||
cfg: float
|
||||
seed: int
|
||||
model_name: str
|
||||
width: int
|
||||
height: int
|
||||
positive: str
|
||||
negative: str
|
||||
|
||||
class TConfig(TypedDict):
|
||||
ksamplers: list
|
||||
output_format: str
|
||||
|
||||
@dataclass
|
||||
class D2_TD2Pipe:
|
||||
ckpt_name: Optional[str] = None
|
||||
positive: Optional[str] = None
|
||||
negative: Optional[str] = None
|
||||
seed: Optional[int] = None
|
||||
steps: Optional[int] = None
|
||||
cfg: Optional[float] = None
|
||||
sampler_name: Optional[str] = None
|
||||
scheduler: Optional[str] = None
|
||||
denoise: Optional[float] = None
|
||||
width: Optional[int] = None
|
||||
height: Optional[int] = None
|
||||
@@ -0,0 +1,14 @@
|
||||
[project]
|
||||
name = "comfyui-d2-send-eagle"
|
||||
description = "Send generated images to Eagle"
|
||||
version = "1.6.1"
|
||||
license = { file = "LICENSE" }
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/da2el-ai/ComfyUI-d2-send-eagle"
|
||||
# Used by Comfy Registry https://comfyregistry.org
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "da2el"
|
||||
DisplayName = "ComfyUI-d2-send-eagle"
|
||||
Icon = ""
|
||||
@@ -0,0 +1 @@
|
||||
piexif
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 436 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 505 KiB |
Reference in New Issue
Block a user