From c2f25dcd41b1888e835a46d4caf2d0d63ebda235 Mon Sep 17 00:00:00 2001 From: Ewald <129623145+MajoorWaldi@users.noreply.github.com> Date: Fri, 1 May 2026 20:03:25 +0200 Subject: [PATCH] Fix live preview media refresh --- README.md | 2 +- __init__.py | 20 ++++++++++++++++++-- js/preview/host.js | 38 +++++++++++++++++++++++++++++++++++++- nodes/channel.py | 10 +++++++++- package.json | 2 +- src/preview/host.ts | 27 ++++++++++++++++++++++++++- 6 files changed, 92 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 2501018..f087435 100644 --- a/README.md +++ b/README.md @@ -623,7 +623,7 @@ All nodes process batches natively: **Author**: Majoor **Category**: `image/imageops` -**Version**: 0.1.2 +**Version**: 0.1.4 --- diff --git a/__init__.py b/__init__.py index 21dfb73..f095c7c 100644 --- a/__init__.py +++ b/__init__.py @@ -70,6 +70,18 @@ def _field_factory(kind: str): return getattr(_node20_io, fallback_map.get(kind, "String"), None) +def _custom_field_factory(type_name: str): + if _node20_io is None: + return None + custom = getattr(_node20_io, "Custom", None) + if callable(custom): + try: + return custom(type_name) + except Exception: + return None + return None + + def _make_schema_input(name: str, spec, optional: bool = False): if _node20_io is None: return None @@ -104,7 +116,9 @@ def _make_schema_input(name: str, spec, optional: bool = False): return factory.Input(name, options=list(raw_type), **kwargs) type_name = str(raw_type or "STRING").upper() - if type_name == "COLOR" or ("COLOR" in name.lower() and type_name == "STRING"): + if "," in type_name or type_name == "VIDEO": + factory = _custom_field_factory(type_name) or _field_factory("Image") + elif type_name == "COLOR" or ("COLOR" in name.lower() and type_name == "STRING"): factory = _field_factory("Color") elif type_name in {"BOOLEAN", "BOOL"}: factory = _field_factory("Boolean") @@ -144,7 +158,9 @@ def _make_schema_output(name: str, output_type: str): if _node20_io is None: return None kind = str(output_type or "STRING").upper() - if kind == "MASK": + if "," in kind or kind == "VIDEO": + factory = _custom_field_factory(kind) or _field_factory("Image") + elif kind == "MASK": factory = _field_factory("Mask") elif "IMAGE" in kind or "VIDEO" in kind: factory = _field_factory("Image") diff --git a/js/preview/host.js b/js/preview/host.js index 5ac4ea1..3316f33 100644 --- a/js/preview/host.js +++ b/js/preview/host.js @@ -417,6 +417,18 @@ function registerImageOpsLivePreview() { startLoopIfVideo(n); } } + function refreshDependentsSoon(changedNode) { + refreshDependents(changedNode); + for (const delayMs of [50, 250]) { + setTimeout(() => { + try { + refreshDependents(changedNode); + } catch (e) { + console.warn("[ImageOps] delayed refreshDependents threw", e); + } + }, delayMs); + } + } function hookNode(node) { if (!IMAGEOPS_CLASSES.has(node.comfyClass)) { if (node.__imageops_hooked_ext) return; @@ -430,12 +442,36 @@ function registerImageOpsLivePreview() { console.warn("[ImageOps] upstream onExecuted threw", e); } try { - refreshDependents(node); + refreshDependentsSoon(node); } catch (e) { console.warn("[ImageOps] refreshDependents threw", e); } return r; }; + for (const w of node.widgets ?? []) { + if (typeof w.callback !== "function" && typeof w.callback !== "undefined") continue; + const orig = w.callback; + w.callback = function() { + const r = orig?.apply(this, arguments); + try { + refreshDependentsSoon(node); + } catch (e) { + console.warn("[ImageOps] widget refreshDependents threw", e); + } + return r; + }; + const element = w.element; + if (element && !w.__imageops_dom_refresh_hooked) { + w.__imageops_dom_refresh_hooked = true; + element.addEventListener?.("change", () => { + try { + refreshDependentsSoon(node); + } catch (e) { + console.warn("[ImageOps] widget change refreshDependents threw", e); + } + }); + } + } return; } const st = ensureState(node); diff --git a/nodes/channel.py b/nodes/channel.py index a722337..30e7b33 100644 --- a/nodes/channel.py +++ b/nodes/channel.py @@ -41,6 +41,14 @@ class ImageOpsChannel: return build_node_preview_result(source, (source, output_mask), prefix="imageops_channel") extracted = _extract_channel_mask(source, channel) - result = _channel_mask_to_image(extracted, source) + if str(_scalar(channel, str)).strip().lower() == "alpha": + result = source.clone() + if result.shape[-1] < 4: + result = _channel_mask_to_image(extracted, source) + else: + result[..., :3] = 1.0 + result[..., 3] = extracted.to(device=result.device, dtype=result.dtype) + else: + result = _channel_mask_to_image(extracted, source) progress.finish() return build_node_preview_result(result, (result, extracted), prefix="imageops_channel") diff --git a/package.json b/package.json index d4688bd..e3f3c31 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "comfyui-majoor-imageops", - "version": "0.1.3", + "version": "0.1.4", "description": "WORK IN PROGRESS", "scripts": { "build": "node esbuild.mjs", diff --git a/src/preview/host.ts b/src/preview/host.ts index 345ac20..bc60c86 100644 --- a/src/preview/host.ts +++ b/src/preview/host.ts @@ -473,6 +473,15 @@ export function registerImageOpsLivePreview(): void { } } + function refreshDependentsSoon(changedNode: ComfyNode): void { + refreshDependents(changedNode); + for (const delayMs of [50, 250]) { + setTimeout(() => { + try { refreshDependents(changedNode); } catch (e) { console.warn("[ImageOps] delayed refreshDependents threw", e); } + }, delayMs); + } + } + function hookNode(node: ComfyNode): void { // Non-ImageOps nodes: only hook onExecuted to refresh downstream ImageOps nodes. // Avoid creating heavy state objects or wrapping widgets on every node in the graph. @@ -483,9 +492,25 @@ export function registerImageOpsLivePreview(): void { node.onExecuted = function (this: any, message: any) { let r: any; try { r = origOnExecuted0?.apply(this, arguments as any); } catch (e) { console.warn("[ImageOps] upstream onExecuted threw", e); } - try { refreshDependents(node); } catch (e) { console.warn("[ImageOps] refreshDependents threw", e); } + try { refreshDependentsSoon(node); } catch (e) { console.warn("[ImageOps] refreshDependents threw", e); } return r; }; + for (const w of (node.widgets ?? [])) { + if (typeof w.callback !== "function" && typeof w.callback !== "undefined") continue; + const orig = w.callback; + w.callback = function (this: any) { + const r = orig?.apply(this, arguments as any); + try { refreshDependentsSoon(node); } catch (e) { console.warn("[ImageOps] widget refreshDependents threw", e); } + return r; + }; + const element = w.element as HTMLElement | null | undefined; + if (element && !(w as any).__imageops_dom_refresh_hooked) { + (w as any).__imageops_dom_refresh_hooked = true; + element.addEventListener?.("change", () => { + try { refreshDependentsSoon(node); } catch (e) { console.warn("[ImageOps] widget change refreshDependents threw", e); } + }); + } + } return; }