This commit is contained in:
Denis 2026-07-19 02:00:56 +02:00 committed by GitHub
commit ba95b07f7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 109 additions and 6 deletions

View File

@ -170,6 +170,19 @@ def is_previewable(media_type: str, item: dict) -> bool:
return False
def is_text_preview(media_type: str, item: dict) -> bool:
"""
Check if a previewable output item is textual rather than visual media.
Saved text files (SaveText's .txt/.md/.json) are real outputs but must not
outrank visual media when picking the job preview.
"""
if media_type == 'text':
return True
filename = item.get('filename', '').lower()
return any(filename.endswith(ext) for ext in TEXT_EXTENSIONS)
def normalize_queue_item(item: tuple, status: str) -> dict:
"""Convert queue item tuple to unified job dict.
@ -259,8 +272,13 @@ def get_outputs_summary(outputs: dict) -> tuple[int, Optional[dict]]:
Returns (outputs_count, preview_output).
Preview priority (matching frontend):
1. type="output" with previewable media
2. Any previewable media
1. type="output" visual media (saved images/video/audio/3d)
2. any other previewable visual media (e.g. temp/preview images)
3. saved text file (e.g. SaveText's .txt/.md/.json)
4. raw text (only when the job produced nothing else previewable)
Text is kept in its own slots so node/execution order can't let a text
output mask a visual one (e.g. a text node that runs before an image).
Text content entries (strings under 'text') are preview-only metadata,
matching the frontend's METADATA_KEYS: they can serve as the fallback
@ -269,6 +287,8 @@ def get_outputs_summary(outputs: dict) -> tuple[int, Optional[dict]]:
count = 0
preview_output = None
fallback_preview = None
text_file_fallback = None
text_fallback = None
for node_id, node_outputs in outputs.items():
if not isinstance(node_outputs, dict):
@ -296,8 +316,8 @@ def get_outputs_summary(outputs: dict) -> tuple[int, Optional[dict]]:
'nodeId': node_id,
'mediaType': media_type
}
if fallback_preview is None:
fallback_preview = enriched
if text_fallback is None:
text_fallback = enriched
continue
# normalize_output_item returned a dict (e.g. 3D file)
item = normalized
@ -314,12 +334,15 @@ def get_outputs_summary(outputs: dict) -> tuple[int, Optional[dict]]:
}
if 'mediaType' not in item:
enriched['mediaType'] = media_type
if item.get('type') == 'output':
if is_text_preview(media_type, item):
if text_file_fallback is None:
text_file_fallback = enriched
elif item.get('type') == 'output':
preview_output = enriched
elif fallback_preview is None:
fallback_preview = enriched
return count, preview_output or fallback_preview
return count, preview_output or fallback_preview or text_file_fallback or text_fallback
def apply_sorting(jobs: list[dict], sort_by: str, sort_order: str) -> list[dict]:

View File

@ -280,6 +280,86 @@ class TestGetOutputsSummary:
assert preview['filename'] == 'model.glb'
assert preview['mediaType'] == '3d'
def test_media_preview_preferred_over_text(self):
"""A visual output wins the preview even when a text node is iterated
first (regression: text could mask a later temp/preview image)."""
outputs = {
'text_node': {'text': ['a caption']},
'image_node': {'images': [{'filename': 'preview.png', 'type': 'temp'}]},
}
count, preview = get_outputs_summary(outputs)
# Text is preview-only metadata and not counted; only the image counts.
assert count == 1
assert preview['filename'] == 'preview.png'
assert preview['mediaType'] == 'images'
def test_text_used_as_preview_when_no_media(self):
"""Text is the preview only when the job produced no media output."""
outputs = {
'text_node': {'text': ['hello world']},
}
count, preview = get_outputs_summary(outputs)
assert count == 0 # text entries are not counted as outputs
assert preview['mediaType'] == 'text'
assert preview['content'] == 'hello world'
def test_media_preview_preferred_over_saved_text_file(self):
"""A visual output wins the preview over a saved text file (SaveText),
even a temp/preview image iterated after the text node."""
outputs = {
'save_text': {
'text': ['the text'],
'files': [{'filename': 'ComfyUI_00001.txt', 'subfolder': '', 'type': 'output'}],
},
'preview_image': {'images': [{'filename': 'preview.png', 'type': 'temp'}]},
}
count, preview = get_outputs_summary(outputs)
assert count == 2 # the .txt file and the image; raw text is metadata
assert preview['filename'] == 'preview.png'
assert preview['mediaType'] == 'images'
def test_saved_media_preferred_over_saved_text_file(self):
outputs = {
'save_text': {
'text': ['the text'],
'files': [{'filename': 'ComfyUI_00001.txt', 'subfolder': '', 'type': 'output'}],
},
'save_image': {'images': [{'filename': 'result.png', 'type': 'output'}]},
}
count, preview = get_outputs_summary(outputs)
assert count == 2
assert preview['filename'] == 'result.png'
def test_mime_format_file_preferred_over_saved_text_file(self):
"""Custom-node outputs previewable via MIME format (e.g. VHS videos
under arbitrary keys) rank as visual media, above saved text files."""
outputs = {
'save_text': {
'files': [{'filename': 'notes.md', 'subfolder': '', 'type': 'output'}],
},
'video_node': {
'files': [{'filename': 'clip.webm', 'format': 'video/webm', 'type': 'output'}],
},
}
count, preview = get_outputs_summary(outputs)
assert count == 2
assert preview['filename'] == 'clip.webm'
def test_saved_text_file_preferred_over_raw_text(self):
"""With no media in the job, the saved text file (a real, counted
output) is the preview rather than the raw text metadata."""
outputs = {
'save_text': {
'text': ['the text'],
'files': [{'filename': 'ComfyUI_00001.txt', 'subfolder': '', 'type': 'output'}],
},
}
count, preview = get_outputs_summary(outputs)
assert count == 1
assert preview['filename'] == 'ComfyUI_00001.txt'
assert preview['mediaType'] == 'files'
class TestHas3DExtension:
"""Unit tests for has_3d_extension()"""