mirror of
https://github.com/Comfy-Org/ComfyUI.git
synced 2026-08-05 10:00:28 +08:00
Add previewable_outputs_count to the jobs API
outputs_count (get_outputs_summary) counts every output item across all nodes for a job, including non-previewable files (e.g. SaveLatent's .latent file). The Media Assets sidebar badge reads outputs_count, but the expanded asset view only ever renders previewable outputs (image, video, audio, 3D, text), so the badge can show a higher number than what a user sees when they drill in. Add count_previewable_outputs(), reusing the existing is_previewable() filter, and expose it as previewable_outputs_count on /api/jobs job entries (list, detail, and queue placeholders). get_outputs_summary() and outputs_count are left untouched so the true total stays available.
This commit is contained in:
@@ -184,6 +184,7 @@ def normalize_queue_item(item: tuple, status: str) -> dict:
|
||||
'priority': priority,
|
||||
'create_time': create_time,
|
||||
'outputs_count': 0,
|
||||
'previewable_outputs_count': 0,
|
||||
'workflow_id': workflow_id,
|
||||
})
|
||||
|
||||
@@ -202,6 +203,7 @@ def normalize_history_item(prompt_id: str, history_item: dict, include_outputs:
|
||||
|
||||
outputs = history_item.get('outputs', {})
|
||||
outputs_count, preview_output = get_outputs_summary(outputs)
|
||||
previewable_outputs_count = count_previewable_outputs(outputs)
|
||||
|
||||
execution_error = None
|
||||
execution_start_time = None
|
||||
@@ -238,6 +240,7 @@ def normalize_history_item(prompt_id: str, history_item: dict, include_outputs:
|
||||
'execution_end_time': execution_end_time,
|
||||
'execution_error': execution_error,
|
||||
'outputs_count': outputs_count,
|
||||
'previewable_outputs_count': previewable_outputs_count,
|
||||
'preview_output': preview_output,
|
||||
'workflow_id': workflow_id,
|
||||
})
|
||||
@@ -322,6 +325,32 @@ def get_outputs_summary(outputs: dict) -> tuple[int, Optional[dict]]:
|
||||
return count, preview_output or fallback_preview
|
||||
|
||||
|
||||
def count_previewable_outputs(outputs: dict) -> int:
|
||||
"""
|
||||
Count only outputs that would actually render in the expanded asset view,
|
||||
i.e. items is_previewable() accepts (image/video/audio/3D/text). Kept
|
||||
separate from get_outputs_summary()'s outputs_count, which counts every
|
||||
output item regardless of media type, so a job with non-previewable
|
||||
outputs (e.g. a raw JSON blob from a custom node) alongside real media
|
||||
doesn't inflate the Media Assets badge beyond what the expanded view shows.
|
||||
"""
|
||||
count = 0
|
||||
for node_outputs in outputs.values():
|
||||
if not isinstance(node_outputs, dict):
|
||||
continue
|
||||
for media_type, items in node_outputs.items():
|
||||
if media_type == 'animated' or not isinstance(items, list):
|
||||
continue
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
item = normalize_output_item(item)
|
||||
if item is None:
|
||||
continue
|
||||
if is_previewable(media_type, item):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def apply_sorting(jobs: list[dict], sort_by: str, sort_order: str) -> list[dict]:
|
||||
"""Sort jobs list by specified field and order."""
|
||||
reverse = (sort_order == 'desc')
|
||||
|
||||
@@ -10,6 +10,7 @@ from comfy_execution.jobs import (
|
||||
normalize_output_item,
|
||||
normalize_outputs,
|
||||
get_outputs_summary,
|
||||
count_previewable_outputs,
|
||||
apply_sorting,
|
||||
has_3d_extension,
|
||||
validate_job_id,
|
||||
@@ -281,6 +282,79 @@ class TestGetOutputsSummary:
|
||||
assert preview['mediaType'] == '3d'
|
||||
|
||||
|
||||
class TestCountPreviewableOutputs:
|
||||
"""Unit tests for count_previewable_outputs()
|
||||
|
||||
Kept separate from get_outputs_summary()'s outputs_count: the Media Assets
|
||||
badge should reflect only what the expanded asset view actually renders
|
||||
(previewable outputs), while outputs_count keeps counting every output
|
||||
item for other consumers.
|
||||
"""
|
||||
|
||||
def test_empty_outputs(self):
|
||||
assert count_previewable_outputs({}) == 0
|
||||
|
||||
def test_previewable_outputs_all_counted(self):
|
||||
"""When every output is previewable, the two counts should match."""
|
||||
outputs = {
|
||||
'node1': {'images': [{'filename': 'a.png', 'type': 'output'}]},
|
||||
'node2': {'images': [{'filename': 'b.png', 'type': 'output'}]},
|
||||
}
|
||||
outputs_count, _ = get_outputs_summary(outputs)
|
||||
assert count_previewable_outputs(outputs) == outputs_count == 2
|
||||
|
||||
def test_save_latent_counted_but_not_previewable(self):
|
||||
"""SaveLatent (nodes.py) emits a real saved file under the 'latents'
|
||||
media type: {'latents': [{'filename': '..._00001_.latent',
|
||||
'subfolder': '', 'type': 'output'}]}. It has no previewable media
|
||||
type, format, or extension, so it inflates outputs_count without
|
||||
ever rendering in the expanded asset view."""
|
||||
outputs = {
|
||||
'node1': {
|
||||
'images': [{'filename': 'ComfyUI_00001_.png', 'subfolder': '', 'type': 'output'}]
|
||||
},
|
||||
'node2': {
|
||||
'latents': [{'filename': 'ComfyUI_00001_.latent', 'subfolder': '', 'type': 'output'}]
|
||||
},
|
||||
}
|
||||
outputs_count, _ = get_outputs_summary(outputs)
|
||||
assert outputs_count == 2
|
||||
assert count_previewable_outputs(outputs) == 1
|
||||
|
||||
def test_save_text_file_output_is_previewable_by_extension(self):
|
||||
"""SaveText (comfy_extras/nodes_text.py) emits its saved file under a
|
||||
'files' media type via ui.SavedResult: {'files': [{'filename':
|
||||
'..._00001.txt', 'subfolder': ..., 'type': 'output'}]}. The .txt
|
||||
extension makes it previewable even though 'files' itself isn't a
|
||||
previewable media type."""
|
||||
outputs = {
|
||||
'node1': {
|
||||
'files': [{'filename': 'ComfyUI_00001.txt', 'subfolder': '', 'type': 'output'}]
|
||||
}
|
||||
}
|
||||
assert count_previewable_outputs(outputs) == 1
|
||||
|
||||
def test_preview_any_text_tuple_not_counted(self):
|
||||
"""PreviewAny (comfy_extras/nodes_preview_any.py) emits only
|
||||
{'text': (value,)} with no saved file. Since the value is a tuple,
|
||||
not a list, it is excluded from both outputs_count and
|
||||
previewable_outputs_count — matching get_outputs_summary()."""
|
||||
outputs = {
|
||||
'node1': {'text': ('some previewed value',)}
|
||||
}
|
||||
outputs_count, _ = get_outputs_summary(outputs)
|
||||
assert outputs_count == 0
|
||||
assert count_previewable_outputs(outputs) == 0
|
||||
|
||||
def test_string_3d_filename_previewable(self):
|
||||
"""String 3D filenames (e.g. Preview3D) normalize into a previewable
|
||||
item just like they do for outputs_count."""
|
||||
outputs = {
|
||||
'node1': {'result': ['preview3d_abc123.glb', None]}
|
||||
}
|
||||
assert count_previewable_outputs(outputs) == 1
|
||||
|
||||
|
||||
class TestHas3DExtension:
|
||||
"""Unit tests for has_3d_extension()"""
|
||||
|
||||
@@ -367,6 +441,7 @@ class TestNormalizeQueueItem:
|
||||
assert 'execution_error' not in job
|
||||
assert 'preview_output' not in job
|
||||
assert job['outputs_count'] == 0
|
||||
assert job['previewable_outputs_count'] == 0
|
||||
assert job['workflow_id'] == 'workflow-abc'
|
||||
|
||||
|
||||
@@ -555,6 +630,54 @@ class TestNormalizeHistoryItem:
|
||||
{'filename': 'photo.png', 'type': 'output', 'subfolder': ''},
|
||||
]
|
||||
|
||||
def test_previewable_outputs_count_excludes_non_previewable_outputs(self):
|
||||
"""Regression test for the Media Assets badge overcount: a job with an
|
||||
image (SaveImage) and a SaveLatent output should report previewable_
|
||||
outputs_count == 1 while outputs_count == 2, so the frontend badge
|
||||
(once switched to previewable_outputs_count) matches what the
|
||||
expanded asset view actually renders."""
|
||||
history_item = {
|
||||
'prompt': (
|
||||
5,
|
||||
'prompt-mixed',
|
||||
{'nodes': {}},
|
||||
{'create_time': 1234567890},
|
||||
['node1', 'node2'],
|
||||
),
|
||||
'status': {'status_str': 'success', 'completed': True, 'messages': []},
|
||||
'outputs': {
|
||||
'node1': {
|
||||
'images': [{'filename': 'ComfyUI_00001_.png', 'subfolder': '', 'type': 'output'}]
|
||||
},
|
||||
'node2': {
|
||||
'latents': [{'filename': 'ComfyUI_00001_.latent', 'subfolder': '', 'type': 'output'}]
|
||||
},
|
||||
},
|
||||
}
|
||||
job = normalize_history_item('prompt-mixed', history_item)
|
||||
|
||||
assert job['outputs_count'] == 2
|
||||
assert job['previewable_outputs_count'] == 1
|
||||
|
||||
def test_previewable_outputs_count_zero_pruned_by_prune_dict(self):
|
||||
"""A job with no outputs at all should still report both counts as 0,
|
||||
not omit the field (prune_dict only strips None, not 0)."""
|
||||
history_item = {
|
||||
'prompt': (
|
||||
5,
|
||||
'prompt-empty',
|
||||
{'nodes': {}},
|
||||
{'create_time': 1234567890},
|
||||
['node1'],
|
||||
),
|
||||
'status': {'status_str': 'success', 'completed': True, 'messages': []},
|
||||
'outputs': {},
|
||||
}
|
||||
job = normalize_history_item('prompt-empty', history_item)
|
||||
|
||||
assert job['outputs_count'] == 0
|
||||
assert job['previewable_outputs_count'] == 0
|
||||
|
||||
|
||||
class TestNormalizeOutputItem:
|
||||
"""Unit tests for normalize_output_item()"""
|
||||
|
||||
Reference in New Issue
Block a user