Add previewable_outputs_count to /api/jobs (backend half of Media Assets badge fix) (#15148)

This commit is contained in:
claude[bot] 2026-08-08 18:06:03 -07:00 committed by GitHub
parent 9eaba63e1a
commit a683fa6e57
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 153 additions and 0 deletions

View File

@ -197,6 +197,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,
})
@ -215,6 +216,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
@ -251,6 +253,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,
})
@ -345,6 +348,33 @@ def get_outputs_summary(outputs: dict) -> tuple[int, Optional[dict]]:
return count, preview_output or fallback_preview or text_file_fallback or text_fallback
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 a non-previewable
saved file alongside real media (e.g. SaveLatent's .latent output next to
a SaveImage output) 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')

View File

@ -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,
@ -361,6 +362,79 @@ class TestGetOutputsSummary:
assert preview['mediaType'] == 'files'
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()"""
@ -447,6 +521,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'
@ -635,6 +710,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()"""