From 978895e1a90bac9dd48017bed83cbdc001ea8d39 Mon Sep 17 00:00:00 2001 From: Yousef Rafat <81116377+yousef-rafat@users.noreply.github.com> Date: Mon, 25 May 2026 17:12:10 +0300 Subject: [PATCH 1/5] Add Save Text Node --- comfy_extras/nodes_dataset.py | 2 +- comfy_extras/nodes_text.py | 64 +++++++++++++++++++++++++++++++++++ nodes.py | 1 + 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 comfy_extras/nodes_text.py diff --git a/comfy_extras/nodes_dataset.py b/comfy_extras/nodes_dataset.py index 73fe75b7f..5fa8ba2e2 100644 --- a/comfy_extras/nodes_dataset.py +++ b/comfy_extras/nodes_dataset.py @@ -1640,4 +1640,4 @@ class DatasetExtension(ComfyExtension): async def comfy_entrypoint() -> DatasetExtension: - return DatasetExtension() + return DatasetExtension() \ No newline at end of file diff --git a/comfy_extras/nodes_text.py b/comfy_extras/nodes_text.py new file mode 100644 index 000000000..64133718c --- /dev/null +++ b/comfy_extras/nodes_text.py @@ -0,0 +1,64 @@ +import os +import json +from typing_extensions import override +from comfy_api.latest import io, ComfyExtension, ui +import folder_paths +import logging + + +class SaveTextNode(io.ComfyNode): + """Save text content to .txt, .md, or .json.""" + + @classmethod + def define_schema(cls): + return io.Schema( + node_id="SaveText", + search_aliases=["save text", "write text", "export text"], + display_name="Save Text", + category="text", + description="Save text content to a file in the output directory.", + inputs=[ + io.String.Input("text", force_input=True), + io.String.Input("filename_prefix", default="ComfyUI"), + io.Combo.Input("format", options=["txt", "md", "json"], default="txt"), + ], + is_output_node=True, + ) + + @classmethod + def execute(cls, text, filename_prefix, format): + full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path( + filename_prefix, + folder_paths.get_output_directory(), + 1, + 1, + ) + + file = f"{filename}_{counter:05}_.{format}" + filepath = os.path.join(full_output_folder, file) + + if format == "json": + # tries to pretty print otherwise saves normally + try: + data = json.loads(text) + with open(filepath, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + except json.JSONDecodeError: + logging.warning("Saved JSON as a raw text") + with open(filepath, "w", encoding="utf-8") as f: + f.write(text) + else: + with open(filepath, "w", encoding="utf-8") as f: + f.write(text) + + return io.NodeOutput(ui=ui.PreviewText(text)) + +class TextExtension(ComfyExtension): + @override + async def get_node_list(self) -> list[type[io.ComfyNode]]: + return [ + SaveTextNode + ] + +async def comfy_entrypoint() -> TextExtension: + return TextExtension() diff --git a/nodes.py b/nodes.py index 9043a8d0a..229ec0197 100644 --- a/nodes.py +++ b/nodes.py @@ -2502,6 +2502,7 @@ async def init_builtin_extra_nodes(): "nodes_triposplat.py", "nodes_depth_anything_3.py", "nodes_seed.py", + "nodes_text.py", ] import_failed = [] From 84f96a56cabd79e1cf96a60b34a01c52154456b6 Mon Sep 17 00:00:00 2001 From: Yousef Rafat <81116377+yousef-rafat@users.noreply.github.com> Date: Mon, 25 May 2026 17:17:56 +0300 Subject: [PATCH 2/5] . --- comfy_extras/nodes_dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comfy_extras/nodes_dataset.py b/comfy_extras/nodes_dataset.py index 5fa8ba2e2..73fe75b7f 100644 --- a/comfy_extras/nodes_dataset.py +++ b/comfy_extras/nodes_dataset.py @@ -1640,4 +1640,4 @@ class DatasetExtension(ComfyExtension): async def comfy_entrypoint() -> DatasetExtension: - return DatasetExtension() \ No newline at end of file + return DatasetExtension() From cd53aff8d8d685cd97bdc48b667f5a463b6f4231 Mon Sep 17 00:00:00 2001 From: "Yousef R. Gamaleldin" <81116377+yousef-rafat@users.noreply.github.com> Date: Fri, 29 May 2026 21:30:36 +0300 Subject: [PATCH 3/5] Update comfy_extras/nodes_text.py Co-authored-by: Alexis Rolland --- comfy_extras/nodes_text.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comfy_extras/nodes_text.py b/comfy_extras/nodes_text.py index 64133718c..2a5743638 100644 --- a/comfy_extras/nodes_text.py +++ b/comfy_extras/nodes_text.py @@ -34,7 +34,7 @@ class SaveTextNode(io.ComfyNode): 1, ) - file = f"{filename}_{counter:05}_.{format}" + file = f"{filename}_{counter:05}.{format}" filepath = os.path.join(full_output_folder, file) if format == "json": From 949c4b8d0733e09a1022ebc9cc495cee4295a876 Mon Sep 17 00:00:00 2001 From: Yousef Rafat <81116377+yousef-rafat@users.noreply.github.com> Date: Fri, 29 May 2026 22:29:54 +0300 Subject: [PATCH 4/5] savedResult --- comfy_extras/nodes_text.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/comfy_extras/nodes_text.py b/comfy_extras/nodes_text.py index 2a5743638..0e261e69b 100644 --- a/comfy_extras/nodes_text.py +++ b/comfy_extras/nodes_text.py @@ -51,7 +51,14 @@ class SaveTextNode(io.ComfyNode): with open(filepath, "w", encoding="utf-8") as f: f.write(text) - return io.NodeOutput(ui=ui.PreviewText(text)) + return io.NodeOutput( + ui={ + "text": (text,), + "files": [ + ui.SavedResult(file, subfolder, io.FolderType.output) + ] + } + ) class TextExtension(ComfyExtension): @override From eda4ba84f2d179bca63366a51d4b51b9d02c49fc Mon Sep 17 00:00:00 2001 From: Terry Jia Date: Fri, 3 Jul 2026 09:04:04 -0400 Subject: [PATCH 5/5] fix(jobs): treat text file outputs as previewable and stop counting text previews --- comfy_execution/jobs.py | 15 ++++++++++++--- comfy_extras/nodes_text.py | 2 ++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/comfy_execution/jobs.py b/comfy_execution/jobs.py index fa3ab0faf..f0ad59f86 100644 --- a/comfy_execution/jobs.py +++ b/comfy_execution/jobs.py @@ -56,6 +56,9 @@ PREVIEWABLE_MEDIA_TYPES = frozenset({'images', 'video', 'audio', '3d', 'text'}) # 3D file extensions for preview fallback (no dedicated media_type exists) THREE_D_EXTENSIONS = frozenset({'.obj', '.fbx', '.gltf', '.glb', '.usdz'}) +# Text file extensions for preview fallback (the formats SaveText can produce) +TEXT_EXTENSIONS = frozenset({'.txt', '.md', '.json'}) + def has_3d_extension(filename: str) -> bool: lower = filename.lower() @@ -143,9 +146,10 @@ def is_previewable(media_type: str, item: dict) -> bool: Maintains backwards compatibility with existing logic. Priority: - 1. media_type is 'images', 'video', 'audio', or '3d' + 1. media_type is 'images', 'video', 'audio', '3d', or 'text' 2. format field starts with 'video/' or 'audio/' 3. filename has a 3D extension (.obj, .fbx, .gltf, .glb, .usdz) + 4. filename has a text extension (.txt, .md, .json, ...) """ if media_type in PREVIEWABLE_MEDIA_TYPES: return True @@ -156,10 +160,12 @@ def is_previewable(media_type: str, item: dict) -> bool: if fmt and (fmt.startswith('video/') or fmt.startswith('audio/')): return True - # Check for 3D files by extension + # Check for 3D and text files by extension filename = item.get('filename', '').lower() if any(filename.endswith(ext) for ext in THREE_D_EXTENSIONS): return True + if any(filename.endswith(ext) for ext in TEXT_EXTENSIONS): + return True return False @@ -255,6 +261,10 @@ def get_outputs_summary(outputs: dict) -> tuple[int, Optional[dict]]: Preview priority (matching frontend): 1. type="output" with previewable media 2. Any previewable media + + Text content entries (strings under 'text') are preview-only metadata, + matching the frontend's METADATA_KEYS: they can serve as the fallback + preview but are not counted as outputs. """ count = 0 preview_output = None @@ -275,7 +285,6 @@ def get_outputs_summary(outputs: dict) -> tuple[int, Optional[dict]]: if normalized is None: # Not a 3D file string — check for text preview if media_type == 'text': - count += 1 if preview_output is None: if isinstance(item, tuple): text_value = item[0] if item else '' diff --git a/comfy_extras/nodes_text.py b/comfy_extras/nodes_text.py index 0e261e69b..d8e68db63 100644 --- a/comfy_extras/nodes_text.py +++ b/comfy_extras/nodes_text.py @@ -22,6 +22,7 @@ class SaveTextNode(io.ComfyNode): io.String.Input("filename_prefix", default="ComfyUI"), io.Combo.Input("format", options=["txt", "md", "json"], default="txt"), ], + outputs=[io.String.Output(display_name="text")], is_output_node=True, ) @@ -52,6 +53,7 @@ class SaveTextNode(io.ComfyNode): f.write(text) return io.NodeOutput( + text, ui={ "text": (text,), "files": [