Add accumulator preview and batch save node
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
const EXTENSION = "ethanfel.prompt_builder.accumulator_preview";
|
||||
const NODE_NAME = "SxCPAccumulatorPreview";
|
||||
const entryCache = new Map();
|
||||
|
||||
function widget(node, name) {
|
||||
return node.widgets?.find((w) => w.name === name);
|
||||
}
|
||||
|
||||
function hideWidget(w) {
|
||||
if (!w) return;
|
||||
if (w.origType === undefined) w.origType = w.type;
|
||||
w.type = "hidden";
|
||||
w.hidden = true;
|
||||
w.computeSize = () => [0, -4];
|
||||
}
|
||||
|
||||
function resizeNode(node) {
|
||||
const size = node.computeSize?.();
|
||||
if (size) node.setSize?.(size);
|
||||
app.graph?.setDirtyCanvas(true, true);
|
||||
}
|
||||
|
||||
function nodeKey(nodeOrId) {
|
||||
return String(typeof nodeOrId === "object" ? nodeOrId?.id : nodeOrId);
|
||||
}
|
||||
|
||||
function isAccumulatorPreviewNode(node) {
|
||||
return node?.comfyClass === NODE_NAME || node?.type === NODE_NAME;
|
||||
}
|
||||
|
||||
function getNodeById(id) {
|
||||
return app.graph?.getNodeById?.(Number(id)) || app.graph?._nodes_by_id?.[id] || app.graph?._nodes_by_id?.[Number(id)];
|
||||
}
|
||||
|
||||
function asArray(value) {
|
||||
if (!value) return [];
|
||||
return Array.isArray(value) ? value : [value];
|
||||
}
|
||||
|
||||
function outputStatus(output) {
|
||||
const status = output?.status;
|
||||
if (Array.isArray(status)) return status[0] || "";
|
||||
return status || "";
|
||||
}
|
||||
|
||||
function outputEntries(output) {
|
||||
const entries = output?.entries;
|
||||
if (!entries) return [];
|
||||
if (Array.isArray(entries) && entries.length === 1 && Array.isArray(entries[0])) return entries[0];
|
||||
return asArray(entries);
|
||||
}
|
||||
|
||||
function entryLabel(entry) {
|
||||
const index = entry?.index ?? "?";
|
||||
const id = entry?.id ? ` ${entry.id}` : "";
|
||||
const image = entry?.has_image ? " image" : " value";
|
||||
const shape = Array.isArray(entry?.shape) && entry.shape.length >= 2 ? ` ${entry.shape[1]}x${entry.shape[0]}` : "";
|
||||
return `#${index}${id}${image}${shape}`.trim();
|
||||
}
|
||||
|
||||
function setStatus(node, status) {
|
||||
if (!node._sxcpAccumulatorStatusWidget) return;
|
||||
node._sxcpAccumulatorStatusWidget.value = status || "no accumulator data";
|
||||
node.setDirtyCanvas?.(true, true);
|
||||
}
|
||||
|
||||
function setEntries(node, entries, status = "") {
|
||||
entries = asArray(entries);
|
||||
entryCache.set(nodeKey(node), entries);
|
||||
node._sxcpAccumulatorEntries = entries;
|
||||
if (node._sxcpEntrySelectWidget) {
|
||||
const labels = entries.map(entryLabel);
|
||||
node._sxcpEntrySelectWidget.options.values = labels.length ? labels : ["no entries"];
|
||||
if (!labels.includes(node._sxcpEntrySelectWidget.value)) {
|
||||
node._sxcpEntrySelectWidget.value = labels[0] || "no entries";
|
||||
}
|
||||
}
|
||||
setStatus(node, status || `${entries.length} entries`);
|
||||
resizeNode(node);
|
||||
}
|
||||
|
||||
function selectedEntry(node) {
|
||||
const entries = entryCache.get(nodeKey(node)) || node._sxcpAccumulatorEntries || [];
|
||||
const selected = widget(node, "selected_entry")?.value || node._sxcpEntrySelectWidget?.value || "";
|
||||
const labels = entries.map(entryLabel);
|
||||
const index = labels.indexOf(selected);
|
||||
return index >= 0 ? entries[index] : entries[0];
|
||||
}
|
||||
|
||||
function storeKey(node) {
|
||||
return String(widget(node, "store_key")?.value || "").trim();
|
||||
}
|
||||
|
||||
async function postJson(path, payload) {
|
||||
const response = await api.fetchApi(path, {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data?.error || response.statusText);
|
||||
return data;
|
||||
}
|
||||
|
||||
async function refreshEntries(node) {
|
||||
const key = storeKey(node);
|
||||
if (!key) {
|
||||
alert("Set the same explicit store_key on the Accumulator and Accumulator Preview first.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await postJson("/sxcp/accumulator/list", {store_key: key});
|
||||
setEntries(node, data.entries || [], data.status || "");
|
||||
} catch (err) {
|
||||
console.error(`[${EXTENSION}] refresh failed`, err);
|
||||
alert(`Refresh failed: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSelected(node) {
|
||||
const key = storeKey(node);
|
||||
if (!key) {
|
||||
alert("Set the same explicit store_key on the Accumulator and Accumulator Preview first.");
|
||||
return;
|
||||
}
|
||||
const entry = selectedEntry(node);
|
||||
if (!entry) {
|
||||
alert("No accumulator entry selected.");
|
||||
return;
|
||||
}
|
||||
const label = entryLabel(entry);
|
||||
if (!confirm(`Delete accumulator entry ${label}?`)) return;
|
||||
try {
|
||||
const data = await postJson("/sxcp/accumulator/delete", {
|
||||
store_key: key,
|
||||
entry_id: entry.id || "",
|
||||
index: entry.id ? 0 : entry.index,
|
||||
clear: false,
|
||||
});
|
||||
setEntries(node, data.entries || [], `${data.status || ""}; deleted=${data.removed || 0}; rerun preview to refresh images`);
|
||||
} catch (err) {
|
||||
console.error(`[${EXTENSION}] delete failed`, err);
|
||||
alert(`Delete failed: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function clearStore(node) {
|
||||
const key = storeKey(node);
|
||||
if (!key) {
|
||||
alert("Set the same explicit store_key on the Accumulator and Accumulator Preview first.");
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Clear all entries from accumulator "${key}"?`)) return;
|
||||
try {
|
||||
const data = await postJson("/sxcp/accumulator/delete", {store_key: key, clear: true});
|
||||
setEntries(node, data.entries || [], `${data.status || ""}; cleared=${data.removed || 0}; rerun preview to refresh images`);
|
||||
} catch (err) {
|
||||
console.error(`[${EXTENSION}] clear failed`, err);
|
||||
alert(`Clear failed: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
function setupNode(node) {
|
||||
hideWidget(widget(node, "delete_action"));
|
||||
hideWidget(widget(node, "delete_entry_id"));
|
||||
hideWidget(widget(node, "delete_index"));
|
||||
|
||||
if (!node._sxcpEntrySelectWidget) {
|
||||
node._sxcpEntrySelectWidget = node.addWidget("combo", "selected_entry", "no entries", () => {}, {values: ["no entries"]});
|
||||
node._sxcpEntrySelectWidget.serialize = false;
|
||||
}
|
||||
if (!node._sxcpAccumulatorStatusWidget) {
|
||||
node._sxcpAccumulatorStatusWidget = node.addWidget("text", "accumulator_status", "no accumulator data", () => {});
|
||||
node._sxcpAccumulatorStatusWidget.serialize = false;
|
||||
}
|
||||
if (!node._sxcpDeleteSelectedButton) {
|
||||
node._sxcpDeleteSelectedButton = node.addWidget("button", "Delete Selected Entry", null, () => deleteSelected(node));
|
||||
}
|
||||
if (!node._sxcpClearButton) {
|
||||
node._sxcpClearButton = node.addWidget("button", "Clear Accumulator", null, () => clearStore(node));
|
||||
}
|
||||
if (!node._sxcpRefreshButton) {
|
||||
node._sxcpRefreshButton = node.addWidget("button", "Refresh Entry List", null, () => refreshEntries(node));
|
||||
}
|
||||
resizeNode(node);
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: EXTENSION,
|
||||
|
||||
async setup() {
|
||||
api.addEventListener("executed", ({detail}) => {
|
||||
const node = getNodeById(detail?.node);
|
||||
if (!isAccumulatorPreviewNode(node)) return;
|
||||
const output = detail?.output || {};
|
||||
setEntries(node, outputEntries(output), outputStatus(output));
|
||||
});
|
||||
},
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (nodeData.name !== NODE_NAME) return;
|
||||
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
const result = onNodeCreated?.apply(this, arguments);
|
||||
setupNode(this);
|
||||
return result;
|
||||
};
|
||||
|
||||
const onConfigure = nodeType.prototype.onConfigure;
|
||||
nodeType.prototype.onConfigure = function () {
|
||||
const result = onConfigure?.apply(this, arguments);
|
||||
queueMicrotask(() => setupNode(this));
|
||||
return result;
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user