81 lines
3.7 KiB
JavaScript
81 lines
3.7 KiB
JavaScript
// "Smooth" graph builder — RIFE frame interpolation as a viewer post-process.
|
|
// Builds an off-screen graph: LoadVideo -> GetVideoComponents -> RIFE VFI ->
|
|
// CreateVideo (fps x multiplier) -> SaveVideo. All core ComfyUI video nodes
|
|
// except RIFE VFI (Fannovel16 ComfyUI-Frame-Interpolation, bundled as the
|
|
// "FrameInterpolation" pack). The source video is staged in input/ by
|
|
// /promptchain/extend/prepare (reused) so LoadVideo can consume it.
|
|
|
|
import { offscreenIdBase } from "./offscreen-graph.js";
|
|
|
|
const EMPTY_WORKFLOW = { last_node_id: 0, last_link_id: 0, nodes: [], links: [], groups: [], config: {}, extra: {}, version: 0.4 };
|
|
|
|
function freshWorkflowId() {
|
|
return globalThis.crypto?.randomUUID ? crypto.randomUUID() : `${Date.now().toString(16)}-smooth`;
|
|
}
|
|
|
|
function setNamedWidget(node, name, value) {
|
|
const w = node?.widgets?.find((x) => x.name === name);
|
|
if (!w) return false;
|
|
if (Array.isArray(w.options?.values) && value != null && !w.options.values.includes(value)) {
|
|
w.options.values.push(value); // a freshly-staged input video isn't in the combo yet
|
|
}
|
|
w.value = value;
|
|
w.callback?.(value);
|
|
return true;
|
|
}
|
|
|
|
// True only when every node the Smooth graph needs is registered (RIFE pack +
|
|
// core video nodes). The caller gates the install offer on RIFE VFI specifically.
|
|
export function smoothNodesAvailable() {
|
|
const reg = window.LiteGraph?.registered_node_types || {};
|
|
return ["RIFE VFI", "LoadVideo", "GetVideoComponents", "CreateVideo", "SaveVideo"].every((t) => reg[t]);
|
|
}
|
|
|
|
// opts: { sourceVideoFilename, multiplier, outFps }
|
|
export function buildSmoothGraph(opts) {
|
|
const LG = window.LiteGraph;
|
|
if (!LG || !smoothNodesAvailable()) return null;
|
|
|
|
const workflowId = freshWorkflowId();
|
|
const graph = new LG.LGraph(structuredClone({ ...EMPTY_WORKFLOW, id: workflowId }));
|
|
graph.last_node_id = offscreenIdBase(); // render-node ids must stay off the live canvas — see offscreen-graph.js
|
|
const mk = (t) => { const n = LG.createNode(t); if (n) graph.add(n); return n; };
|
|
|
|
const load = mk("LoadVideo");
|
|
const comp = mk("GetVideoComponents");
|
|
const rife = mk("RIFE VFI");
|
|
const create = mk("CreateVideo");
|
|
const save = mk("SaveVideo");
|
|
if (!load || !comp || !rife || !create || !save) return null;
|
|
|
|
setNamedWidget(load, "file", opts.sourceVideoFilename);
|
|
setNamedWidget(rife, "ckpt_name", opts.ckptName || "rife49.pth");
|
|
setNamedWidget(rife, "multiplier", opts.multiplier || 2);
|
|
// Quality/VRAM knobs — all optional, RIFE's own defaults if omitted. A picked
|
|
// ckpt that isn't on disk auto-downloads on first run (the node handles it).
|
|
if (opts.scaleFactor != null) setNamedWidget(rife, "scale_factor", opts.scaleFactor);
|
|
if (opts.ensemble != null) setNamedWidget(rife, "ensemble", opts.ensemble);
|
|
if (opts.fastMode != null) setNamedWidget(rife, "fast_mode", opts.fastMode);
|
|
if (!setNamedWidget(create, "fps", opts.outFps || 32) && create.widgets?.[1]) {
|
|
create.widgets[1].value = opts.outFps || 32; // positional fallback (images, fps, audio)
|
|
}
|
|
if (!setNamedWidget(save, "filename_prefix", "video/PromptChain") && save.widgets?.[0]) {
|
|
save.widgets[0].value = "video/PromptChain";
|
|
}
|
|
|
|
// wiring: VIDEO -> components; images -> RIFE; interpolated -> CreateVideo;
|
|
// carry the original audio through; CreateVideo -> SaveVideo.
|
|
try {
|
|
load.connect(0, comp, 0); // VIDEO -> GetVideoComponents.video
|
|
comp.connect(0, rife, 0); // images -> RIFE.frames
|
|
rife.connect(0, create, 0); // interpolated images -> CreateVideo.images
|
|
comp.connect(1, create, 1); // audio -> CreateVideo.audio (optional)
|
|
create.connect(0, save, 0); // VIDEO -> SaveVideo.video
|
|
} catch (e) {
|
|
console.warn("[Smooth] wiring failed", e);
|
|
return null;
|
|
}
|
|
|
|
return { graph, workflowId };
|
|
}
|