Files
vllm-omni/apps/ComfyUI-vLLM-Omni/web/main.js
T

156 lines
6.2 KiB
JavaScript

/**
* @file Front-end extension for the vLLM-Omni nodes.
*
* Branding. Every vLLM-Omni node draws the vLLM-Omni "v" mark where LiteGraph
* would otherwise draw its default title dot, and is tinted by the kind of value
* it produces: blue nodes reach a server, amber and purple ones only assemble
* parameters, red ones pin what the server has loaded, teal ones carry media.
* A graph is then readable at a glance, and a node that turns out to be the
* wrong colour for where it sits is usually wired wrong.
*
* The tint is keyed off each node's declared output types rather than a list of
* node names, so a node added later is themed as soon as it returns one of the
* existing types.
*
* It also migrates Generate Video graphs saved before `duration` replaced the
* `num_frames` widget. See migrateNumFramesToDuration below.
*
* This file also used to host dynamic fields on vLLM-Omni nodes, driven by
* widget (in-node form fields) and input (connection link) values and changes.
* That is deliberately absent: it introduced too much complexity, and it even
* conflicted with the current backend (Python) validation for unknown reasons
* (pending ComfyUI upstream fixes).
*/
import { app } from "../../scripts/app.js";
const MARK_URL = new URL("./vllm-omni-mark.svg", import.meta.url).href;
const MARK_ASPECT = 157 / 165;
/**
* Title bar and body tints, one per node family.
*
* The hues are the logo's: the amber and blue of the "v" mark, and three stops
* of the gradient ring in the "O" (#9128A3, #CC5260, and the teal neighbouring
* its blue). All of them are dark because LiteGraph paints the title bar darker
* than the body and expects both to sit under light text; the ComfyUI light
* theme lightens custom node colours on its own.
*/
const FAMILIES = {
generate: { color: "#152A43", bgcolor: "#1F4269" },
sampling: { color: "#3A2C12", bgcolor: "#5C461C" },
model: { color: "#2E1233", bgcolor: "#4A1C52" },
deployment: { color: "#3A1A1F", bgcolor: "#5C2931" },
media: { color: "#0F2E2A", bgcolor: "#194A44" },
};
/** Output type -> family. Types absent here fall through to "generate". */
const FAMILY_BY_OUTPUT = {
SAMPLING_PARAMS: "sampling",
TTS_PARAMS: "model",
VIDEO_PARAMS: "model",
REMOTE_LORA: "deployment",
FASTH3_DEPLOYMENT: "deployment",
VIDEO_REFERENCES: "media",
};
function familyOf(nodeData) {
for (const type of nodeData.output ?? []) {
const family = FAMILY_BY_OUTPUT[type];
if (family) {
return family;
}
}
// What is left returns a generated artifact (IMAGE, VIDEO, AUDIO, STRING),
// which is only ever produced by a node that talks to a vLLM-Omni server.
return "generate";
}
/**
* Rewrite a pre-rename Generate Video graph in place.
*
* `duration` (seconds) took over the slot `num_frames` used to occupy, and
* ComfyUI restores widget values positionally, so a graph saved before the
* rename reads its frame count as a duration: the workflow this repository
* shipped stored 120 frames at 24 fps, which would come back as a 120-second
* request. Saved files still name the old widget under `inputs`, and that is
* what makes the rewrite both detectable and safe to apply exactly once -- once
* rewritten the node serializes `duration`, so a later load finds nothing to do.
*/
function migrateNumFramesToDuration(node, info) {
const savedBeforeRename = (info?.inputs ?? []).some((input) => input?.widget?.name === "num_frames");
if (!savedBeforeRename) {
return
}
const duration = node.widgets?.find((widget) => widget.name === "duration");
const fps = node.widgets?.find((widget) => widget.name === "fps");
if (!duration || !fps) {
return
}
// configure() has already placed the stale frame count in the duration widget.
const frames = Number(duration.value);
const rate = Number(fps.value);
if (!Number.isFinite(frames) || !Number.isFinite(rate) || rate <= 0) {
return
}
const seconds = Math.max(duration.options?.min ?? 0.1, Math.round((frames / rate) * 1000) / 1000);
console.info(
`vLLM-Omni: migrated Generate Video "${node.title}" from ${frames} frames to ${seconds}s at ${rate} fps.`,
);
duration.value = seconds;
}
const mark = new Image();
let markReady = false;
mark.addEventListener("load", () => {
markReady = true;
// The image resolves after the first frames are already on screen.
app.graph?.setDirtyCanvas?.(true, true);
});
mark.src = MARK_URL;
app.registerExtension({
name: "vllm.vllm_omni",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (!nodeData.name.startsWith("VLLMOmni")) {
return
}
const { color, bgcolor } = FAMILIES[familyOf(nodeData)];
// On the prototype, not the instance: a colour the user picks by hand is
// written to the instance and shadows this, so manual overrides survive.
nodeType.prototype.color = color;
nodeType.prototype.bgcolor = bgcolor;
if (nodeData.name === "VLLMOmniGenerateVideo") {
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function (info) {
onConfigure?.apply(this, arguments);
migrateNumFramesToDuration(this, info);
};
}
// Defining this replaces the default dot outright — LiteGraph draws its
// own square or circle only for nodes that leave onDrawTitleBox unset.
nodeType.prototype.onDrawTitleBox = function (ctx, titleHeight, _size, scale) {
// Zoomed out far enough that LiteGraph itself drops to flat shapes,
// the mark is a few muddy pixels. Leave the title bar clean instead.
if (!markReady || (scale !== undefined && scale < 0.5)) {
return
}
const height = titleHeight * 0.62;
const width = height * MARK_ASPECT;
ctx.drawImage(
mark,
titleHeight * 0.5 - width * 0.5,
titleHeight * -0.5 - height * 0.5,
width,
height,
);
};
},
async setup() {
console.info("vLLM-Omni Setup complete!")
},
})