109 lines
7.7 KiB
JavaScript
109 lines
7.7 KiB
JavaScript
// Interpolate a finished render with NVIDIA RTX Frame Generation: more frames, same length and sound; the result replaces the render. Never / button beside the rating / always.
|
|
import { composer as c } from "../../composer/composer.js";
|
|
import { inPlace } from "../../shell/place.js";
|
|
import { genUnitId, isGenerative, unitRoot } from "../../shell/scenes.js";
|
|
import { derived, keyOf, onGaveUp, swap, wentThrough } from "../../shell/renders.js";
|
|
import { DEFAULTS, RTX, interpolate, modeLabel } from "./job.js";
|
|
|
|
export default {
|
|
id: "interpolate",
|
|
mount: "timeline.actions",
|
|
needs: ["project", "api", "selection"],
|
|
modules: ["interpolate"],
|
|
setup({ host, app }) {
|
|
const p = app.project, sel = app.selection;
|
|
const put = inPlace(host), busy = new Set();
|
|
const render = (id) => ((p.project || {}).scene_renders || {})[id];
|
|
const target = () => { // the picked clip's generation unit root, when it has a render
|
|
const open = p.project, sc = open && open.scenes.find((s) => s.id === sel.focus);
|
|
const root = sc && isGenerative(sc) ? unitRoot(open, genUnitId(sc)) : null;
|
|
const r = root && render(root.id);
|
|
return r && r.media ? r.media : null;
|
|
};
|
|
const opts = () => ({ ...DEFAULTS, ...(p.pref("interpolateOpts", {}) || {}) });
|
|
// Pressed with nothing to interpolate with: say what would fix it, and take the person there.
|
|
async function suggest() {
|
|
let all = []; try { all = await app.api.addons(); } catch { /* no add-on list */ }
|
|
const fg = all.find((a) => a.id === "nvidia_framegen");
|
|
if (!fg) return void c.toast.warn({ text: `${RTX} needs an NVIDIA RTX card on Windows or Linux, so it is not available here.` });
|
|
const pick = await c.modal.choice({ title: "Nothing to interpolate with yet", items: [fg.state === "pending"
|
|
? { id: "modules", label: `${RTX} is switched on`, hint: "Restart ComfyUI to install it." }
|
|
: { id: "modules", label: `Install ${RTX}`, hint: "Switch it on in Settings ▸ Modules. It needs nvidia-vfx 0.2 or newer and moves an older one up." }] }).result;
|
|
if (pick) app.openSettings(pick);
|
|
}
|
|
|
|
let told = false;
|
|
async function run(media, pressed = false) {
|
|
if (!media || wentThrough(media, "interpolate")) return; // never interpolate twice
|
|
const key = keyOf(media), pid = p.project.id;
|
|
if (busy.has(key)) return;
|
|
busy.add(key); draw();
|
|
try {
|
|
// Only counts when this machine has it on right now: a project saved elsewhere may still say "always"
|
|
if (!(await app.api.frameGeneration().then((r) => Boolean(r.rtx)).catch(() => false)))
|
|
return void (pressed ? suggest() : told || (told = true, c.toast.warn({ text: `${RTX} is not available on this machine (or is switched off in Settings ▸ Modules). Interpolate "always" is skipped.` }))); // once, not at every render
|
|
c.toast.info({ text: `Interpolating with ${RTX}…` });
|
|
const out = await interpolate(app.api, media, opts());
|
|
derived(media, out, "interpolate");
|
|
const swapped = await p.editFor(pid, (pr) => swap(pr, media, out) > 0);
|
|
if (!swapped) c.toast.warn({ text: "Interpolated, but that render was replaced meanwhile, so nothing was swapped." });
|
|
else c.toast.good({ text: `Interpolated: the render now plays at ${opts().multiplier} times the frames.` });
|
|
} catch (err) { c.toast[err.stopped ? "info" : "warn"]({ text: err.stopped ? "Interpolate stopped. The render stays as it was." : `Interpolate failed: ${err.message}` }); }
|
|
finally { busy.delete(key); draw(); }
|
|
}
|
|
|
|
function draw() {
|
|
const media = p.pref("interpolateMode", "never") === "button" ? target() : null;
|
|
if (!media) return put();
|
|
const done = wentThrough(media, "interpolate"), working = busy.has(keyOf(media));
|
|
put(c.button.sm({ label: working ? "Interpolating…" : done ? "Interpolated" : "Interpolate", tone: "ghost", disabled: working || done,
|
|
title: "Add in-between frames to this video with NVIDIA RTX Frame Generation; it replaces the render", onClick: () => run(media, true) }).node);
|
|
}
|
|
|
|
// Upscale goes first when both are on "always"; when it cannot (no model, no RTX, an error) the render is interpolated as it is
|
|
onGaveUp((media, step) => { if (step === "upscale" && p.pref("interpolateMode", "never") === "always") run(media); });
|
|
|
|
// "Always": a render that appears after the project was opened is interpolated. What was there on open is left alone.
|
|
let seen = null, obj = null;
|
|
const watch = () => {
|
|
const open = p.project;
|
|
if (!open) { seen = null; obj = null; return; }
|
|
const now = new Map(Object.entries(open.scene_renders || {}).filter(([, r]) => r && r.media).map(([id, r]) => [id, keyOf(r.media)]));
|
|
if (open !== obj || !seen) { obj = open; seen = now; return; } // another project, or undo/redo put an older copy back: not a new render
|
|
if (p.pref("interpolateMode", "never") === "always") {
|
|
const fresh = new Map([...now].filter(([id, k]) => seen.get(id) !== k));
|
|
const pending = (m) => p.pref("upscaleMode", "never") === "always" && !wentThrough(m, "upscale"); // Upscale goes first; its result comes back as a new render
|
|
for (const media of new Map([...fresh.keys()].map((id) => [fresh.get(id), open.scene_renders[id].media])).values()) if (!pending(media)) run(media);
|
|
}
|
|
seen = now;
|
|
};
|
|
|
|
app.editorSettings.push(() => {
|
|
const page = c.region.stack({ gap: "md" });
|
|
const set = (k, v) => { p.setPref("interpolateOpts", { ...opts(), [k]: v }); draw(); };
|
|
const paint = (r) => {
|
|
const o = opts();
|
|
page.set([
|
|
c.label.section({ text: "Interpolate finished renders" }),
|
|
c.hint.default({ text: "Adds in-between frames to a finished video and replaces the render with the result. The length and the sound stay, the frame rate goes up." }),
|
|
c.settingsRow.default({ label: "Interpolate", hint: "Never, with a button beside the rating, or on every new render.", control: c.select.md({ label: "Interpolate", value: p.pref("interpolateMode", "never"),
|
|
options: [{ value: "never", label: "Never" }, { value: "button", label: "When I press Interpolate" }, { value: "always", label: "Always, when a render finishes" }], onChange: (v) => { p.setPref("interpolateMode", v); draw(); } }) }),
|
|
...(r && r.rtx ? [
|
|
c.settingsRow.default({ label: "Frames", hint: "2 times turns 24 fps into 48 fps.", control: c.select.md({ label: "Frames", value: String(o.multiplier), onChange: (v) => set("multiplier", Number(v)),
|
|
options: Array.from({ length: r.rtx.max_multiplier - 1 }, (_, i) => ({ value: String(i + 2), label: `${i + 2} times` })) }) }),
|
|
c.settingsRow.default({ label: "Quality", hint: "Fast is for a quick look, Best for the final video.", control: c.select.md({ label: "Quality", value: o.mode, onChange: (v) => set("mode", v),
|
|
options: r.rtx.modes.map((m) => ({ value: m, label: modeLabel(m) })) }) }),
|
|
c.toggle.default({ label: "10-bit processing", hint: "More precise colour inside the interpolation. The saved video stays 8-bit.", checked: o.tenbit, onChange: (v) => set("tenbit", v) }),
|
|
] : r === null ? [] : [c.banner.warn({ text: !r.rtx_offered ? `${RTX} needs an NVIDIA RTX card on Windows or Linux, so it is not available here.` : `${RTX} is not installed or is switched off: switch it on in Settings ▸ Modules and restart ComfyUI.` })]),
|
|
]);
|
|
};
|
|
paint(null);
|
|
app.api.frameGeneration().then(paint).catch(() => page.set([c.banner.warn({ text: "Could not read the frame generation setup." })]));
|
|
return page;
|
|
});
|
|
|
|
draw(); watch();
|
|
return app.on(() => { watch(); draw(); });
|
|
},
|
|
};
|