Files
ausboss-ComfyUI-AusBoss/js/shared/input_preview.mjs
T
ausboss 0e4749db6b Preview each node's own result, and rework Save Video
Five things surfaced by release testing.

Node search: ComfyUI's search dialog cuts at 64 results and ranks by
[match tier, global popularity, name length], where tier 1 needs the name
to START with what was typed. Popularity is a fixed global table in which
every node from a new pack scores zero, so tier is the only lever. As
"Refine Mask" this node came 121st for "mask" and was never on screen.
Renamed to Mask Refine, which is 15th, and kept "refine mask" as an alias.
The mapping key is untouched, so saved workflows are unaffected. Aliases
alone cannot fix this: the frontend indexes them but drops them from the
tier scoring because they are a list rather than a string, so they decide
whether a node is findable, not where it lands.

Previews: Mask Refine, LaMa Inpaint and Select Frame showed whatever fed
their input, which is nothing when a segmentation node is upstream - Mask
Refine's panel was blank however the mask turned out. Each now returns its
result as a preview and the panel shows that, falling back to the input
before the first run. A node does not need to be an OUTPUT_NODE for this;
returning a ui payload is enough. ComfyUI's own preview for these nodes is
stood down so the picture appears once, in the panel, rather than also
underneath the node. That also fixes LaMa finishing a single image with an
empty panel: frame previews were only attached for batches over one frame,
on the assumption that one image would preview through the output path,
which a non-output node does not have.

Mask Refine opens on expand and blur with the other five behind MORE.
Hidden widgets keep their values and their saved positions. AUTO sets both
from the mask's size, scaled off the 8/4 hand-tuned for the watermark
workflow at 576px, because a feather is a fraction of the picture rather
than a fixed pixel count.

Save Video gains nvenc h264/h265, webm av1, ProRes mov, lossless ffv1 mkv,
gif and webp, plus pingpong and save_metadata. Every format is encoded and
read back in the tests rather than trusted from its table row. fps steps in
whole frames but stays FLOAT: an INT would refuse Load Video's fps link and
round 29.97 to 30, drifting picture off audio. mov files now carry the
embedded workflow, which they silently did not. The reload button is gone -
it only re-fetched what was already on screen.

Not done: Select Frame's preview updates on a run, not as the number
changes. Seeking to a frame needs the source fps, which the browser never
learns, so live scrubbing needs a server route - the surface Frame Chooser
was just demoted for.
2026-08-18 16:31:44 -05:00

90 lines
3.7 KiB
JavaScript

// Pure resolution rules for the upstream-input preview panel. DOM-free so
// node:test covers them without a browser; the wiring lives in
// js/input_preview/index.js.
//
// Best-effort order for a connected source node:
// 1. cached execution previews (sourceNode.imgs[0].src)
// 2. an uploadable image/video file combo widget -> deterministic /view query
// 3. nothing -> the caller shows a quiet placeholder
import { splitMediaName } from "./video_preview.mjs";
// Core loader nodes keep the uploaded file name in a combo widget with one of
// these names (LoadImage uses "image", video loaders use "video").
const FILE_WIDGET_NAMES = new Set(["image", "video"]);
// Graph link stores differ by frontend generation: a Map on the modern
// frontend, a plain id-keyed object on legacy LiteGraph.
export function linkById(graph, linkId) {
const links = graph?.links;
if (links == null || linkId == null) return null;
const link = typeof links.get === "function" ? links.get(linkId) : links[linkId];
return link ?? null;
}
// Walk the named input's link back to the node feeding it, or null.
export function upstreamNode(node, inputName) {
const graph = node?.graph;
if (!graph) return null;
const input = node.inputs?.find((candidate) => candidate?.name === inputName);
const link = linkById(graph, input?.link);
if (!link) return null;
return graph.getNodeById?.(link.origin_id) ?? null;
}
// The widget whose callback fires when the user picks a different file.
export function sourceFileWidget(sourceNode) {
return sourceNode?.widgets?.find((widget) => FILE_WIDGET_NAMES.has(widget?.name)) ?? null;
}
// Deterministic /view query (no cache buster) for an input-folder file name,
// tolerating Windows separators and " [input]" annotations.
export function viewQueryForFile(name) {
const { filename, subfolder } = splitMediaName(name);
return new URLSearchParams({ filename, subfolder, type: "input" }).toString();
}
// Describe how to preview a source node:
// { kind: "url", url } cached preview image
// { kind: "view", isVideo, query } file widget, serve via api /view
// null nothing resolvable
export function describeSourcePreview(sourceNode) {
if (!sourceNode) return null;
const cached = sourceNode.imgs?.[0]?.src;
if (typeof cached === "string" && cached) return { kind: "url", url: cached };
const widget = sourceFileWidget(sourceNode);
if (widget && typeof widget.value === "string" && widget.value) {
return {
kind: "view",
isVideo: widget.name === "video",
query: viewQueryForFile(widget.value),
};
}
return null;
}
// The node's own latest result, if it has produced one.
//
// imgs is where the frontend leaves both the streamed progress frames of a
// running node and the images from its ui payload, so the last entry is the
// most recent thing this node made. The panel prefers it over the upstream
// input for the obvious reason: once a node has a result, its result is what
// you want to look at.
export function describeOwnResult(node) {
const images = node?.imgs;
if (!Array.isArray(images) || images.length === 0) return null;
const src = images[images.length - 1]?.src;
return typeof src === "string" && src ? { kind: "url", url: src } : null;
}
// What the panel shows, best first: this node's result, then whatever feeds
// it, then nothing.
export function describeNodePreview(node, inputName) {
return describeOwnResult(node) ?? describeSourcePreview(upstreamNode(node, inputName));
}
// Quiet ASCII placeholder copy; never an error.
export function placeholderText(connected, noun = "an image") {
return connected ? "run to preview" : `connect ${noun} to preview`;
}