320 lines
9.4 KiB
JavaScript
320 lines
9.4 KiB
JavaScript
const { app } = window.comfyAPI.app;
|
|
const { api } = window.comfyAPI.api;
|
|
|
|
function findWidget(node, name) {
|
|
return (node.widgets || []).find((widget) => widget.name === name);
|
|
}
|
|
|
|
function findInput(node, name) {
|
|
return (node.inputs || []).find((input) => input.name === name);
|
|
}
|
|
|
|
function getOriginNodeForInput(node, inputName) {
|
|
const input = findInput(node, inputName);
|
|
if (!input || input.link == null) return null;
|
|
const link = app.graph.links[input.link];
|
|
if (!link) return null;
|
|
return app.graph.getNodeById(link.origin_id);
|
|
}
|
|
|
|
function coercePreviewUrl(value) {
|
|
if (!value) return "";
|
|
if (typeof value === "string") return value;
|
|
if (typeof value === "object") {
|
|
if (typeof value.src === "string") return value.src;
|
|
if (typeof value.url === "string") return value.url;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function resolveFileWidgetUrlFromOrigin(originNode) {
|
|
if (!originNode) return "";
|
|
|
|
const widgets = originNode.widgets || [];
|
|
const fileWidget = widgets.find((widget) => ["image", "filename", "file"].includes(widget.name));
|
|
const subfolderWidget = widgets.find((widget) => widget.name === "subfolder");
|
|
const filename = typeof fileWidget?.value === "string" ? fileWidget.value : "";
|
|
const subfolder = typeof subfolderWidget?.value === "string" ? subfolderWidget.value : "";
|
|
if (!filename) return "";
|
|
|
|
return api.apiURL(
|
|
`/view?filename=${encodeURIComponent(filename)}&type=input&subfolder=${encodeURIComponent(subfolder)}`
|
|
);
|
|
}
|
|
|
|
function resolvePreviewUrlFromOrigin(originNode) {
|
|
if (!originNode) return "";
|
|
const preview = Array.isArray(originNode.imgs) ? coercePreviewUrl(originNode.imgs[0]) : "";
|
|
return preview || "";
|
|
}
|
|
|
|
function resolveCandidateImageUrls(originNode) {
|
|
const urls = [];
|
|
const fileUrl = resolveFileWidgetUrlFromOrigin(originNode);
|
|
const previewUrl = resolvePreviewUrlFromOrigin(originNode);
|
|
if (fileUrl) urls.push(fileUrl);
|
|
if (previewUrl && previewUrl !== fileUrl) urls.push(previewUrl);
|
|
return urls;
|
|
}
|
|
|
|
function setWidgetValue(node, widget, value) {
|
|
if (!widget) return;
|
|
const previous = widget.value;
|
|
widget.value = value;
|
|
if (widget.element) {
|
|
if (widget.element.type === "checkbox") {
|
|
widget.element.checked = !!value;
|
|
} else {
|
|
widget.element.value = value;
|
|
}
|
|
}
|
|
if (widget.callback) {
|
|
try {
|
|
widget.callback(value);
|
|
} catch (error) {}
|
|
}
|
|
if (node.onWidgetChanged) {
|
|
try {
|
|
node.onWidgetChanged(widget.name, value, previous, widget);
|
|
} catch (error) {}
|
|
}
|
|
if (node.properties) {
|
|
node.properties[widget.name] = value;
|
|
}
|
|
}
|
|
|
|
function syncFormFromWidgets(node) {
|
|
const description = findWidget(node, "description")?.value || "";
|
|
const alias = findWidget(node, "alias")?.value || "";
|
|
|
|
if (node._msrAliasInput && node._msrAliasInput.value !== alias) {
|
|
node._msrAliasInput.value = alias;
|
|
}
|
|
if (node._msrDescriptionInput && node._msrDescriptionInput.value !== description) {
|
|
node._msrDescriptionInput.value = description;
|
|
}
|
|
if (node._msrMeta) {
|
|
node._msrMeta.innerHTML = [
|
|
alias
|
|
? `Alias: <span style="color:#e8e8e8">@${String(alias).replace(/^@/, "")}</span>`
|
|
: "Alias: <span style=\"color:#666\">none</span>",
|
|
`<div style="margin-top:4px;color:#666">Description is stored in the field below and appended to the director prompt as a tagged character reference.</div>`,
|
|
].join("");
|
|
}
|
|
}
|
|
|
|
function refreshCharacterPreview(node) {
|
|
if (!node._msrPreviewRow) return;
|
|
|
|
const inputNames = ["image_1", "image_2"];
|
|
const previewUrls = inputNames
|
|
.map((inputName) => {
|
|
const originNode = getOriginNodeForInput(node, inputName);
|
|
const candidates = resolveCandidateImageUrls(originNode);
|
|
return candidates[0] || "";
|
|
})
|
|
.filter(Boolean);
|
|
|
|
const previewRow = node._msrPreviewRow;
|
|
previewRow.innerHTML = "";
|
|
|
|
if (previewUrls.length) {
|
|
previewUrls.forEach((previewUrl) => {
|
|
const image = document.createElement("img");
|
|
image.src = previewUrl;
|
|
Object.assign(image.style, {
|
|
flex: "1",
|
|
minWidth: "0",
|
|
height: "74px",
|
|
objectFit: "cover",
|
|
borderRadius: "6px",
|
|
border: "1px solid #444",
|
|
background: "#111",
|
|
});
|
|
previewRow.appendChild(image);
|
|
});
|
|
} else {
|
|
const placeholder = document.createElement("div");
|
|
placeholder.textContent = "Connect up to 2 image nodes";
|
|
Object.assign(placeholder.style, {
|
|
width: "100%",
|
|
height: "74px",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
color: "#777",
|
|
fontSize: "11px",
|
|
borderRadius: "6px",
|
|
border: "1px dashed #444",
|
|
background: "#181818",
|
|
});
|
|
previewRow.appendChild(placeholder);
|
|
}
|
|
|
|
syncFormFromWidgets(node);
|
|
}
|
|
|
|
function buildCharacterUi(node) {
|
|
const container = document.createElement("div");
|
|
Object.assign(container.style, {
|
|
boxSizing: "border-box",
|
|
width: "100%",
|
|
padding: "6px 2px 2px 2px",
|
|
});
|
|
|
|
const previewRow = document.createElement("div");
|
|
Object.assign(previewRow.style, {
|
|
display: "flex",
|
|
gap: "6px",
|
|
minHeight: "74px",
|
|
});
|
|
|
|
const meta = document.createElement("div");
|
|
Object.assign(meta.style, {
|
|
marginTop: "8px",
|
|
fontSize: "11px",
|
|
color: "#aaa",
|
|
lineHeight: "1.4",
|
|
});
|
|
|
|
const form = document.createElement("div");
|
|
Object.assign(form.style, {
|
|
marginTop: "10px",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: "6px",
|
|
});
|
|
|
|
const aliasLabel = document.createElement("label");
|
|
aliasLabel.textContent = "Alias";
|
|
Object.assign(aliasLabel.style, {
|
|
fontSize: "11px",
|
|
color: "#bbb",
|
|
});
|
|
|
|
const aliasInput = document.createElement("input");
|
|
aliasInput.type = "text";
|
|
aliasInput.placeholder = "optional alias, e.g. bob";
|
|
Object.assign(aliasInput.style, {
|
|
width: "100%",
|
|
boxSizing: "border-box",
|
|
background: "#1c1c1c",
|
|
color: "#eee",
|
|
border: "1px solid #444",
|
|
borderRadius: "6px",
|
|
padding: "6px 8px",
|
|
fontSize: "12px",
|
|
});
|
|
aliasInput.addEventListener("input", () => {
|
|
setWidgetValue(node, findWidget(node, "alias"), aliasInput.value);
|
|
syncFormFromWidgets(node);
|
|
});
|
|
|
|
const descLabel = document.createElement("label");
|
|
descLabel.textContent = "Description";
|
|
Object.assign(descLabel.style, {
|
|
fontSize: "11px",
|
|
color: "#bbb",
|
|
});
|
|
|
|
const descInput = document.createElement("textarea");
|
|
descInput.placeholder = "Character description...";
|
|
Object.assign(descInput.style, {
|
|
width: "100%",
|
|
minHeight: "86px",
|
|
boxSizing: "border-box",
|
|
resize: "vertical",
|
|
background: "#1c1c1c",
|
|
color: "#eee",
|
|
border: "1px solid #444",
|
|
borderRadius: "6px",
|
|
padding: "8px",
|
|
fontSize: "12px",
|
|
lineHeight: "1.35",
|
|
});
|
|
descInput.addEventListener("input", () => {
|
|
setWidgetValue(node, findWidget(node, "description"), descInput.value);
|
|
syncFormFromWidgets(node);
|
|
});
|
|
|
|
form.appendChild(aliasLabel);
|
|
form.appendChild(aliasInput);
|
|
form.appendChild(descLabel);
|
|
form.appendChild(descInput);
|
|
|
|
container.appendChild(previewRow);
|
|
container.appendChild(meta);
|
|
container.appendChild(form);
|
|
|
|
node._msrPreviewContainer = container;
|
|
node._msrPreviewRow = previewRow;
|
|
node._msrMeta = meta;
|
|
node._msrAliasInput = aliasInput;
|
|
node._msrDescriptionInput = descInput;
|
|
return container;
|
|
}
|
|
|
|
function hideNodeWidget(widget) {
|
|
if (!widget) return;
|
|
widget.hidden = true;
|
|
if (!widget.options) widget.options = {};
|
|
widget.options.hidden = true;
|
|
if (widget.element) {
|
|
widget.element.style.display = "none";
|
|
}
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "Comfy.MSRCharacterCS",
|
|
async beforeRegisterNodeDef(nodeType, nodeData) {
|
|
if (nodeData.name === "MSRCharacterCS") {
|
|
const originalOnNodeCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function () {
|
|
if (originalOnNodeCreated) originalOnNodeCreated.apply(this, arguments);
|
|
|
|
if (!this._msrPreviewWidget) {
|
|
const previewContainer = buildCharacterUi(this);
|
|
this._msrPreviewWidget = this.addDOMWidget("msr_character_ui", "msr_character_ui", previewContainer, {
|
|
getValue: () => "",
|
|
setValue: () => {},
|
|
});
|
|
this._msrPreviewWidget.computeSize = () => [Math.max(10, (this.size?.[0] || 320) - 30), 260];
|
|
}
|
|
|
|
const originalConnectionsChange = this.onConnectionsChange;
|
|
this.onConnectionsChange = function () {
|
|
const result = originalConnectionsChange?.apply(this, arguments);
|
|
refreshCharacterPreview(this);
|
|
return result;
|
|
};
|
|
|
|
const originalConfigure = this.onConfigure;
|
|
this.onConfigure = function () {
|
|
const result = originalConfigure?.apply(this, arguments);
|
|
setTimeout(() => {
|
|
syncFormFromWidgets(this);
|
|
refreshCharacterPreview(this);
|
|
}, 0);
|
|
return result;
|
|
};
|
|
|
|
["alias", "description"].forEach((widgetName) => {
|
|
hideNodeWidget(findWidget(this, widgetName));
|
|
});
|
|
|
|
this.size[0] = Math.max(this.size[0] || 0, 330);
|
|
syncFormFromWidgets(this);
|
|
refreshCharacterPreview(this);
|
|
};
|
|
}
|
|
|
|
if (nodeData.name === "MSRCharacterSetCS") {
|
|
const originalOnNodeCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function () {
|
|
if (originalOnNodeCreated) originalOnNodeCreated.apply(this, arguments);
|
|
this.size[0] = Math.max(this.size[0] || 0, 260);
|
|
};
|
|
}
|
|
},
|
|
});
|