diff --git a/README.jp.md b/README.jp.md index f529b86..bf7d501 100644 --- a/README.jp.md +++ b/README.jp.md @@ -16,6 +16,33 @@ git clone https://github.com/nkchocoai/ComfyUI-SaveImageWithMetaData.git - メタデータは `sampler_selection_method` で見つけたKSamplerノードの入力と以前に実行されたノードの入力から取得します。 - 対象となるKSamplerノードは[py/defs/samplers.py](py/defs/samplers.py)と[py/defs/ext/](py/defs/ext/)配下のファイルの`SAMPLERS`のキーです。 +#### filename_prefix +- `filename_prefix` で指定した文字列(Key)は取得した情報に置換されます。 + +| Key | 置換先の情報 | +| --------------------- | -------------------------- | +| %seed% | シード値 | +| %width% | 画像の幅 | +| %height% | 画像の高さ | +| %pprompt% | Positive Prompt | +| %pprompt:<文字数n>% | Positive Promptの先頭n文字 | +| %nprompt% | Negative Prompt | +| %nprompt:<文字数n>% | Negative Promptの先頭n文字 | +| %model% | Checkpoint名 | +| %model:<文字数n>% | Checkpoint名の先頭n文字 | +| %date:<フォーマット>% | 生成日時 | + +- `%date:<フォーマット>%` の `<フォーマット>` で指定する識別子は以下の表を参照ください。 + +| 識別子 | 説明 | +| ------ | ---- | +| yyyy | 年 | +| MM | 月 | +| dd | 日 | +| hh | 時 | +| mm | 分 | +| ss | 秒 | + #### sampler_selection_method - このノードよりも前に実行されたKSamplerノードを選ぶ方法を指定します。 @@ -65,4 +92,4 @@ git clone https://github.com/nkchocoai/ComfyUI-SaveImageWithMetaData.git - [py/defs/captures.py](py/defs/captures.py) - [py/defs/samplers.py](py/defs/samplers.py) - 対応している拡張機能は以下のディレクトリをご確認ください。 - - [py/defs/ext/](py/defs/ext/) \ No newline at end of file + - [py/defs/ext/](py/defs/ext/) diff --git a/README.md b/README.md index 574bfa4..1b3997e 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,33 @@ git clone https://github.com/nkchocoai/ComfyUI-SaveImageWithMetaData.git - Metadata is extracted from the input of the KSampler node found by `sampler_selection_method` and the input of the previously executed node. - Target KSampler nodes are the key of `SAMPLERS` in the file [py/defs/samplers.py](py/defs/samplers.py) and the file in [py/defs/ext/](py/defs/ext/). +#### filename_prefix +- The string (Key) specified in `filename_prefix` will be replaced with the retrieved information. + +| Key | Information to be replaced | +| --------------- | ------------------------------------- | +| %seed% | seed value | +| %width% | Image width | +| %height% | Image height | +| %pprompt% | Positive Prompt | +| %pprompt:[n]% | first n characters of Positive Prompt | +| %nprompt% | Negative Prompt | +| %nprompt:[n]% | First n characters of Negative Prompt | +| %model% | Checkpoint name | +| %model:[n]% | First n characters of Checkpoint name | +| %date:[format]% | Date of generation | + +- See the following table for the identifier specified by `[format]` in `%date:[format]%`. + +| Identifier | Description | +| ---------- | ----------- | +| yyyy | year | +| MM | month | +| dd | day | +| hh | hour | +| mm | minute | +| ss | second | + #### sampler_selection_method - Specifies how to select a KSampler node that has been executed before this node. @@ -67,4 +94,4 @@ git clone https://github.com/nkchocoai/ComfyUI-SaveImageWithMetaData.git - [py/defs/captures.py](py/defs/captures.py) - [py/defs/samplers.py](py/defs/samplers.py) - Please check the following directories for supported extensions. - - [py/defs/ext/](py/defs/ext/) \ No newline at end of file + - [py/defs/ext/](py/defs/ext/) diff --git a/examples/filename_format.json b/examples/filename_format.json new file mode 100644 index 0000000..088a2a6 --- /dev/null +++ b/examples/filename_format.json @@ -0,0 +1,643 @@ +{ + "last_node_id": 20, + "last_link_id": 29, + "nodes": [ + { + "id": 7, + "type": "PromptUtilitiesConstStringMultiLine", + "pos": [ + 21, + 378 + ], + "size": { + "0": 287.9851379394531, + "1": 99.69133758544922 + }, + "flags": {}, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 7 + ], + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "PromptUtilitiesConstStringMultiLine" + }, + "widgets_values": [ + "(low quality, worst quality:1.4), embedding:EasyNegative" + ] + }, + { + "id": 9, + "type": "CLIPTextEncode", + "pos": [ + 394, + 382 + ], + "size": { + "0": 210, + "1": 54 + }, + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": null, + "slot_index": 0 + }, + { + "name": "text", + "type": "STRING", + "link": 7, + "widget": { + "name": "text" + }, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 10 + ], + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "" + ] + }, + { + "id": 8, + "type": "CLIPTextEncode", + "pos": [ + 390, + 257 + ], + "size": { + "0": 210, + "1": 54 + }, + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": null + }, + { + "name": "text", + "type": "STRING", + "link": 8, + "widget": { + "name": "text" + } + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 9 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "" + ] + }, + { + "id": 10, + "type": "Prompts Everywhere", + "pos": [ + 674, + 302 + ], + "size": { + "0": 210, + "1": 46 + }, + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "name": "CONDITIONING", + "type": "*", + "link": 9, + "color_on": "#FFA931" + }, + { + "name": "CONDITIONING", + "type": "*", + "link": 10, + "slot_index": 1, + "color_on": "#FFA931" + } + ], + "properties": { + "Node name for S&R": "Prompts Everywhere", + "group_restricted": false, + "color_restricted": false + }, + "widgets_values": [] + }, + { + "id": 6, + "type": "PromptUtilitiesConstStringMultiLine", + "pos": [ + 18, + 225 + ], + "size": { + "0": 287.9851379394531, + "1": 99.69133758544922 + }, + "flags": {}, + "order": 1, + "mode": 0, + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 8 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "PromptUtilitiesConstStringMultiLine" + }, + "widgets_values": [ + "masterpiece, best quality, hatsune miku, upper body" + ] + }, + { + "id": 1, + "type": "CheckpointLoaderSimple", + "pos": [ + 24, + 54 + ], + "size": { + "0": 315, + "1": 98 + }, + "flags": {}, + "order": 2, + "mode": 0, + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 24 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 25 + ], + "shape": 3, + "slot_index": 1 + }, + { + "name": "VAE", + "type": "VAE", + "links": [ + 26 + ], + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple" + }, + "widgets_values": [ + "CounterfeitV30_v30.safetensors" + ] + }, + { + "id": 5, + "type": "Anything Everywhere3", + "pos": [ + 395, + 54 + ], + "size": { + "0": 210, + "1": 66 + }, + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "MODEL", + "type": "*", + "link": 24, + "slot_index": 0, + "color_on": "#B39DDB" + }, + { + "name": "CLIP", + "type": "*", + "link": 25, + "color_on": "#FFD500" + }, + { + "name": "VAE", + "type": "*", + "link": 26, + "color_on": "#FF6E6E", + "slot_index": 2 + } + ], + "properties": { + "Node name for S&R": "Anything Everywhere3", + "group_restricted": false, + "color_restricted": false + }, + "widgets_values": [] + }, + { + "id": 2, + "type": "VAELoader", + "pos": [ + 402, + 858 + ], + "size": { + "0": 323.17578125, + "1": 58 + }, + "flags": {}, + "order": 3, + "mode": 0, + "outputs": [ + { + "name": "VAE", + "type": "VAE", + "links": [ + 13 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "VAELoader" + }, + "widgets_values": [ + "clearvae_nanslesstest.safetensors" + ] + }, + { + "id": 13, + "type": "VAEDecode", + "pos": [ + 790, + 531 + ], + "size": { + "0": 210, + "1": 46 + }, + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": 28, + "slot_index": 0 + }, + { + "name": "vae", + "type": "VAE", + "link": 13 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 15 + ], + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "VAEDecode" + } + }, + { + "id": 12, + "type": "EmptyLatentImageFromPresetsSD15", + "pos": [ + 23, + 550 + ], + "size": { + "0": 327.6000061035156, + "1": 122 + }, + "flags": {}, + "order": 4, + "mode": 0, + "outputs": [ + { + "name": "latent", + "type": "LATENT", + "links": [ + 11 + ], + "shape": 3, + "slot_index": 0 + }, + { + "name": "w", + "type": "INT", + "links": null, + "shape": 3 + }, + { + "name": "h", + "type": "INT", + "links": null, + "shape": 3 + } + ], + "properties": { + "Node name for S&R": "EmptyLatentImageFromPresetsSD15" + }, + "widgets_values": [ + "512 x 768", + 4 + ] + }, + { + "id": 11, + "type": "KSampler", + "pos": [ + 402, + 532 + ], + "size": { + "0": 315, + "1": 262 + }, + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "MODEL", + "link": null + }, + { + "name": "positive", + "type": "CONDITIONING", + "link": null + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": null + }, + { + "name": "latent_image", + "type": "LATENT", + "link": 11 + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 28 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "KSampler" + }, + "widgets_values": [ + 123, + "fixed", + 20, + 8, + "dpmpp_2m", + "karras", + 1 + ] + }, + { + "id": 20, + "type": "PromptUtilitiesConstStringMultiLine", + "pos": [ + 800, + 645 + ], + "size": [ + 255.52172323794503, + 110.68967742638506 + ], + "flags": {}, + "order": 5, + "mode": 0, + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 29 + ], + "shape": 3, + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "PromptUtilitiesConstStringMultiLine" + }, + "widgets_values": [ + "siwm-%model:10%/%pprompt:20%-%nprompt:20%/%seed%-%width%x%height%_%date:yyyyMMdd-hhmmss%" + ] + }, + { + "id": 15, + "type": "SaveImageWithMetaData", + "pos": [ + 1132, + 495 + ], + "size": [ + 277.5890906467905, + 411.8033565804974 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 15, + "slot_index": 0 + }, + { + "name": "filename_prefix", + "type": "STRING", + "link": 29, + "widget": { + "name": "filename_prefix" + } + } + ], + "properties": { + "Node name for S&R": "SaveImageWithMetaData" + }, + "widgets_values": [ + "siwm-%model:10%/%pprompt:20%-%nprompt:20%/%seed%-%width%x%height%_%date:yyyyMMdd-hhmmss%", + "Farthest", + 0 + ] + } + ], + "links": [ + [ + 7, + 7, + 0, + 9, + 1, + "STRING" + ], + [ + 8, + 6, + 0, + 8, + 1, + "STRING" + ], + [ + 9, + 8, + 0, + 10, + 0, + "*" + ], + [ + 10, + 9, + 0, + 10, + 1, + "*" + ], + [ + 11, + 12, + 0, + 11, + 3, + "LATENT" + ], + [ + 13, + 2, + 0, + 13, + 1, + "VAE" + ], + [ + 15, + 13, + 0, + 15, + 0, + "IMAGE" + ], + [ + 24, + 1, + 0, + 5, + 0, + "*" + ], + [ + 25, + 1, + 1, + 5, + 1, + "*" + ], + [ + 26, + 1, + 2, + 5, + 2, + "*" + ], + [ + 28, + 11, + 0, + 13, + 0, + "LATENT" + ], + [ + 29, + 20, + 0, + 15, + 1, + "STRING" + ] + ], + "groups": [], + "config": {}, + "extra": {}, + "version": 0.4 +} diff --git a/examples/filename_format.png b/examples/filename_format.png new file mode 100644 index 0000000..32e311b Binary files /dev/null and b/examples/filename_format.png differ diff --git a/py/nodes/node.py b/py/nodes/node.py index 75b8a96..f38acc3 100644 --- a/py/nodes/node.py +++ b/py/nodes/node.py @@ -1,5 +1,8 @@ import json import os +import re + +from datetime import datetime from PIL import Image from PIL.PngImagePlugin import PngInfo @@ -45,6 +48,8 @@ class SaveImageWithMetaData(BaseNode): OUTPUT_NODE = True + pattern_format = re.compile(r"(%[^%]+%)") + def save_images( self, images, @@ -54,23 +59,15 @@ class SaveImageWithMetaData(BaseNode): prompt=None, extra_pnginfo=None, ): - ( - full_output_folder, - filename, - counter, - subfolder, - filename_prefix, - ) = folder_paths.get_save_image_path( - filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0] + pnginfo_dict_src = self.gen_pnginfo( + sampler_selection_method, sampler_selection_node_id ) results = list() for index, image in enumerate(images): i = 255.0 * image.cpu().numpy() img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) - pnginfo_dict = self.gen_pnginfo( - sampler_selection_method, sampler_selection_node_id - ) + pnginfo_dict = pnginfo_dict_src.copy() if len(images) >= 2: pnginfo_dict["Batch index"] = index pnginfo_dict["Batch size"] = len(images) @@ -88,7 +85,21 @@ class SaveImageWithMetaData(BaseNode): for x in extra_pnginfo: metadata.add_text(x, json.dumps(extra_pnginfo[x])) + filename_prefix = self.format_filename(filename_prefix, pnginfo_dict) + output_path = os.path.join(self.output_dir, filename_prefix) + if not os.path.exists(os.path.dirname(output_path)): + os.makedirs(os.path.dirname(output_path), exist_ok=True) + ( + full_output_folder, + filename, + counter, + subfolder, + filename_prefix, + ) = folder_paths.get_save_image_path( + filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0] + ) file = f"{filename}_{counter:05}_.png" + img.save( os.path.join(full_output_folder, file), pnginfo=metadata, @@ -130,3 +141,52 @@ class SaveImageWithMetaData(BaseNode): inputs_before_sampler_node, inputs_before_this_node ) return pnginfo_dict + + @classmethod + def format_filename(cls, filename, pnginfo_dict): + result = re.findall(cls.pattern_format, filename) + for segment in result: + parts = segment.replace("%", "").split(":") + key = parts[0] + if key == "seed": + filename = filename.replace(segment, str(pnginfo_dict.get("Seed", ""))) + elif key == "width": + w = pnginfo_dict.get("Size", "x").split("x")[0] + filename = filename.replace(segment, str(w)) + elif key == "height": + w = pnginfo_dict.get("Size", "x").split("x")[1] + filename = filename.replace(segment, str(w)) + elif key == "pprompt": + prompt = pnginfo_dict.get("Positive prompt", "").replace("\n", " ") + if len(parts) >= 2: + length = int(parts[1]) + prompt = prompt[:length] + filename = filename.replace(segment, prompt.strip()) + elif key == "nprompt": + prompt = pnginfo_dict.get("Negative prompt", "").replace("\n", " ") + if len(parts) >= 2: + length = int(parts[1]) + prompt = prompt[:length] + filename = filename.replace(segment, prompt.strip()) + elif key == "model": + model = pnginfo_dict.get("Model", "") + model = os.path.splitext(os.path.basename(model))[0] + if len(parts) >= 2: + length = int(parts[1]) + model = model[:length] + filename = filename.replace(segment, model) + elif key == "date" and len(parts) >= 2: + date_format = parts[1] + now = datetime.now() + date_table = { + "yyyy": now.year, + "MM": now.hour, + "dd": now.day, + "hh": now.hour, + "mm": now.minute, + "ss": now.second, + } + for k, v in date_table.items(): + date_format = date_format.replace(k, str(v).zfill(len(k))) + filename = filename.replace(segment, date_format) + return filename