- Replace the multi-root folder_paths.workflows workflow library with a scalar workflow_path setting (Settings > Library); remove the extra_folder_paths.workflows path and the standalone Model Paths editor. - Migrate pre-release settings.json files that still keep folder_paths.workflows / extra_folder_paths.workflows on startup, so an upgrade keeps the configured folder instead of silently losing it. - Add "Create LM-managed workflow" to the ComfyUI Graph menu and the File menu/command palette: it writes the serialized graph into the configured folder, stamps extra.lm_source and registers the entry in the scanner cache without forcing a rescan. - Attach the decode image of the client's last executed prompt as the workflow preview. The prompt id is sent with the create request and the backend only honors it while it is still the registry's current prompt, so a tab never borrows another tab's output; otherwise the card keeps the placeholder preview. - Detect library entries deleted/renamed while a graph still carries the marker: a link-status endpoint clears the stale extra.lm_source (on graph load, menu open and before create/update), and the write-back route returns 404 instead of recreating the document. - Collapse the workflows.noPaths standalone/ComfyUI split into a single description/hint; the folder is edited in Settings > Library in both modes, so the manual settings.json guidance and the standalone open-settings-folder fallback are gone. - Add the preview tensor conversion as a shared utility (reused by the recipe widget save path) and cover create, preview gating, link status, migration and menu injection with tests.
69 lines
2.1 KiB
Python
69 lines
2.1 KiB
Python
"""Conversion of ComfyUI decode image tensors to PNG bytes.
|
|
|
|
Shared by the recipe "Save Recipe" flow and the workflow preview creation
|
|
flow: both read the first decoded image out of the metadata registry, which
|
|
is a torch tensor (or numpy-compatible array) rather than encoded bytes.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import logging
|
|
from typing import Any, Optional
|
|
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def tensor_to_png_bytes(image_data: Any) -> Optional[bytes]:
|
|
"""Serialize a ComfyUI IMAGE value to PNG bytes.
|
|
|
|
Accepts the registry's raw shape (a tensor, a numpy array, a one-element
|
|
tuple/list, or a batched array). Returns ``None`` for unsupported shapes
|
|
and never raises.
|
|
"""
|
|
|
|
try:
|
|
if isinstance(image_data, tuple):
|
|
tensor_image = image_data[0] if image_data else None
|
|
if tensor_image is None:
|
|
return None
|
|
else:
|
|
tensor_image = image_data
|
|
|
|
if hasattr(tensor_image, "shape"):
|
|
logger.debug(
|
|
"Tensor shape: %s, dtype: %s",
|
|
tensor_image.shape,
|
|
getattr(tensor_image, "dtype", None),
|
|
)
|
|
|
|
import torch # pyright: ignore[reportMissingImports]
|
|
|
|
if isinstance(tensor_image, torch.Tensor):
|
|
image_np = tensor_image.cpu().numpy()
|
|
else:
|
|
image_np = np.array(tensor_image)
|
|
|
|
while len(image_np.shape) > 3:
|
|
image_np = image_np[0]
|
|
|
|
if image_np.dtype in (np.float32, np.float64) and image_np.max() <= 1.0:
|
|
image_np = (image_np * 255).astype(np.uint8)
|
|
|
|
if len(image_np.shape) == 3 and image_np.shape[2] == 3:
|
|
pil_image = Image.fromarray(image_np)
|
|
img_byte_arr = io.BytesIO()
|
|
pil_image.save(img_byte_arr, format="PNG")
|
|
return img_byte_arr.getvalue()
|
|
except Exception as exc: # pragma: no cover - defensive logging path
|
|
logger.error("Error processing image data: %s", exc, exc_info=True)
|
|
return None
|
|
|
|
return None
|
|
|
|
|
|
__all__ = ["tensor_to_png_bytes"]
|