This commit is contained in:
Yousef R. Gamaleldin 2026-07-05 22:54:35 -04:00 committed by GitHub
commit 7e34b70ae6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 86 additions and 3 deletions

View File

@ -56,6 +56,9 @@ PREVIEWABLE_MEDIA_TYPES = frozenset({'images', 'video', 'audio', '3d', 'text'})
# 3D file extensions for preview fallback (no dedicated media_type exists) # 3D file extensions for preview fallback (no dedicated media_type exists)
THREE_D_EXTENSIONS = frozenset({'.obj', '.fbx', '.gltf', '.glb', '.usdz'}) 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: def has_3d_extension(filename: str) -> bool:
lower = filename.lower() lower = filename.lower()
@ -143,9 +146,10 @@ def is_previewable(media_type: str, item: dict) -> bool:
Maintains backwards compatibility with existing logic. Maintains backwards compatibility with existing logic.
Priority: 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/' 2. format field starts with 'video/' or 'audio/'
3. filename has a 3D extension (.obj, .fbx, .gltf, .glb, .usdz) 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: if media_type in PREVIEWABLE_MEDIA_TYPES:
return True 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/')): if fmt and (fmt.startswith('video/') or fmt.startswith('audio/')):
return True return True
# Check for 3D files by extension # Check for 3D and text files by extension
filename = item.get('filename', '').lower() filename = item.get('filename', '').lower()
if any(filename.endswith(ext) for ext in THREE_D_EXTENSIONS): if any(filename.endswith(ext) for ext in THREE_D_EXTENSIONS):
return True return True
if any(filename.endswith(ext) for ext in TEXT_EXTENSIONS):
return True
return False return False
@ -255,6 +261,10 @@ def get_outputs_summary(outputs: dict) -> tuple[int, Optional[dict]]:
Preview priority (matching frontend): Preview priority (matching frontend):
1. type="output" with previewable media 1. type="output" with previewable media
2. Any 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 count = 0
preview_output = None preview_output = None
@ -275,7 +285,6 @@ def get_outputs_summary(outputs: dict) -> tuple[int, Optional[dict]]:
if normalized is None: if normalized is None:
# Not a 3D file string — check for text preview # Not a 3D file string — check for text preview
if media_type == 'text': if media_type == 'text':
count += 1
if preview_output is None: if preview_output is None:
if isinstance(item, tuple): if isinstance(item, tuple):
text_value = item[0] if item else '' text_value = item[0] if item else ''

View File

@ -0,0 +1,73 @@
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"),
],
outputs=[io.String.Output(display_name="text")],
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(
text,
ui={
"text": (text,),
"files": [
ui.SavedResult(file, subfolder, io.FolderType.output)
]
}
)
class TextExtension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [
SaveTextNode
]
async def comfy_entrypoint() -> TextExtension:
return TextExtension()

View File

@ -2502,6 +2502,7 @@ async def init_builtin_extra_nodes():
"nodes_triposplat.py", "nodes_triposplat.py",
"nodes_depth_anything_3.py", "nodes_depth_anything_3.py",
"nodes_seed.py", "nodes_seed.py",
"nodes_text.py",
] ]
import_failed = [] import_failed = []