- New nodes: Image Resize plus the Utility group (Text, Integer, Float, Show Text, Math Expression, Select Every Nth, Split Batch, Merge Batches, Free Memory) - the dependency-free gaps that let shared workflows drop heavy packs. - Replace with AusBoss nodes (prototype): preview-then-confirm graph rewrite swapping known third-party nodes for pack equivalents, with a tested translation map, per-node opt-out, and refuse-with-reason for nodes the pack cannot stand in for. - Load Video: single-frame FRAME picker, an honest "what will load" label from the probed source rate, drop-a-video-to-restore. Save Video: format-aware widgets and a file_path output. Stitch Inpaint gained a blend_mask output, Select Frame negative from-the-end indexing, Image Size a batch count - all appended, saved workflows load unchanged. - LoRA Loader: drag-to-reorder rows, hover thumbnails on rows, file size and date in the info card. - Appearance: flagship scheme deepened to slate-teal; the retired bright-teal pair upgrades automatically via LEGACY_SCHEME_PAIRS. Optional queue-completion chime, off by default. - Image Resize invariant: pixels are only invented on an explicit width+height pad request - scale-derived modes resolve divisible_by snaps with a sub-half-step resize, never bars. - Docs refreshed for six stale pages, stale text and metadata fixed across the pack, the panel-height rule codified in AGENTS.md and the brand skill, and ausboss_node_tour.json added - a model-free example exercising 17 node types in one graph. Validated: scripts/validate_nodes.py (24 files), 300 JS tests, all Python suites under the ComfyUI embedded interpreter.
94 lines
3.6 KiB
JavaScript
94 lines
3.6 KiB
JavaScript
// What a video dropped ONTO a Load Video 🆎 node restores. An AusBoss-saved
|
|
// file carries the workflow that made it; if that workflow holds a Load
|
|
// Video node, its trim and sampling values repopulate the target node so the
|
|
// clip reloads the way it was loaded before. Pure parsing and sanitizing
|
|
// only - the drop wiring lives in js/video_workflow_drop/index.js and
|
|
// node:test drives this module directly.
|
|
|
|
export const LOAD_VIDEO_CLASS = "AUSBOSS_NODES_LoadVideo";
|
|
|
|
// The node's serialized widget order, i.e. its INPUT_TYPES order. The DOM
|
|
// preview widget never serializes (serialize: false), so widgets_values is
|
|
// exactly this list - and the node only ever appends widgets, for the same
|
|
// positional-compatibility reason this list is append-only.
|
|
export const LOAD_VIDEO_WIDGET_ORDER = [
|
|
"video",
|
|
"start_seconds",
|
|
"end_seconds",
|
|
"custom_width",
|
|
"custom_height",
|
|
"every_nth",
|
|
"max_frames",
|
|
"single_frame",
|
|
];
|
|
|
|
// Widget ranges straight from INPUT_TYPES. The values come out of a file
|
|
// anyone could have written, so each is clamped before it may touch a node.
|
|
const NUMERIC_RANGES = {
|
|
start_seconds: { min: 0, max: 86400, integer: false },
|
|
end_seconds: { min: 0, max: 86400, integer: false },
|
|
custom_width: { min: 0, max: 16384, integer: true },
|
|
custom_height: { min: 0, max: 16384, integer: true },
|
|
every_nth: { min: 1, max: 512, integer: true },
|
|
max_frames: { min: 0, max: 100000, integer: true },
|
|
};
|
|
|
|
// Mirrors VIDEO_EXTENSIONS in nodes/_media_helpers.py - the set the node's
|
|
// own file list accepts, so a drop takes exactly what the picker offers.
|
|
const VIDEO_FILE_PATTERN = /\.(avi|m2ts|m4v|mkv|mov|mp4|mpeg|mpg|mts|webm)$/i;
|
|
|
|
export function isVideoFileName(name) {
|
|
return VIDEO_FILE_PATTERN.test(String(name || ""));
|
|
}
|
|
|
|
function sanitizedNumber(name, value) {
|
|
const range = NUMERIC_RANGES[name];
|
|
const number = Number(value);
|
|
if (!range || !Number.isFinite(number)) return undefined;
|
|
const bounded = Math.min(range.max, Math.max(range.min, number));
|
|
return range.integer ? Math.round(bounded) : bounded;
|
|
}
|
|
|
|
function namedValues(raw) {
|
|
const byName = {};
|
|
if (Array.isArray(raw)) {
|
|
raw.forEach((value, index) => {
|
|
const name = LOAD_VIDEO_WIDGET_ORDER[index];
|
|
if (name) byName[name] = value;
|
|
});
|
|
} else if (raw && typeof raw === "object") {
|
|
for (const name of LOAD_VIDEO_WIDGET_ORDER) {
|
|
if (name in raw) byName[name] = raw[name];
|
|
}
|
|
}
|
|
return byName;
|
|
}
|
|
|
|
// name -> sanitized value for the first Load Video node in the workflow, or
|
|
// null when there is none (or nothing usable on it). "video" is never
|
|
// restored: the dropped file itself is the source now. A save from before
|
|
// the single_frame widget existed was a trim, so the toggle restores off
|
|
// rather than staying whatever the target node happened to have.
|
|
export function loadVideoRestoreValues(workflow) {
|
|
const nodes = Array.isArray(workflow?.nodes) ? workflow.nodes : [];
|
|
for (const node of nodes) {
|
|
if (node?.type !== LOAD_VIDEO_CLASS) continue;
|
|
const named = namedValues(node.widgets_values);
|
|
const restored = {};
|
|
for (const [name, value] of Object.entries(named)) {
|
|
if (name === "video") continue;
|
|
if (name === "single_frame") {
|
|
restored[name] = value === true || value === 1 || value === "true";
|
|
continue;
|
|
}
|
|
const number = sanitizedNumber(name, value);
|
|
if (number !== undefined) restored[name] = number;
|
|
}
|
|
// A restore needs at least the IN point to mean anything.
|
|
if (!("start_seconds" in restored)) continue;
|
|
if (!("single_frame" in restored)) restored.single_frame = false;
|
|
return restored;
|
|
}
|
|
return null;
|
|
}
|