New kg_krea_v10 package with KG Krea 2 Image Guide Card V10 and KG Krea 2 Reference Stack Encoder V10. V9 is untouched; packets stay V9-compatible and the two versions cross-connect via the shared KG_KREA_REFERENCE link type. Card: quick recipes for the four previously manual-only roles (palette, environment, framing, mood board), guide direction (counter-example guidance via negative delta targets with symmetric layer clamping), per-card timing, and manual structure/finish layer dials. Stack: balance budget for hot multi-card stacks, content-keyed study cache that makes strength/timing tweaks compose-only, a plain-language stack report output, and a prepared-reference contact sheet output. The full guard's prompt rewriter gains an ES/FR/DE/PT/IT starter vocabulary. Web extension covers the V10 nodes; tests/test_krea_v10.py pins the V10 label surface, packet compatibility, direction/timing/balance math, cache behavior, and the multilingual guard.
245 lines
7.2 KiB
JavaScript
245 lines
7.2 KiB
JavaScript
// ComfyUI frontend extension for the KG Krea 2 V9 and V10 nodes.
|
|
//
|
|
// Stack encoder: keeps the reference-card inputs compact while the backend
|
|
// still validates up to 12 cards.
|
|
//
|
|
// Guide card: greys out the manual-only rows whenever "Use image for" is a
|
|
// quick recipe, so it is visible at a glance that only "manual tuning" reads
|
|
// those widgets.
|
|
import { app } from "/scripts/app.js";
|
|
|
|
const REFERENCE_TYPE = "KG_KREA_REFERENCE";
|
|
const MAX_REFERENCE_CARDS = 12;
|
|
|
|
const STACK_NODES = {
|
|
KGTextEncodeKreaImageReferencesV9: {
|
|
marker: "__kgKreaV9StackUi",
|
|
patched: "__kgKreaV9StackPatched",
|
|
inputPattern: /^Reference \d+ guide card$/,
|
|
inputName: (i) => `Reference ${i} guide card`,
|
|
},
|
|
KGTextEncodeKreaImageReferencesV10: {
|
|
marker: "__kgKreaV10StackUi",
|
|
patched: "__kgKreaV10StackPatched",
|
|
inputPattern: /^Reference \d+ guide card$/,
|
|
inputName: (i) => `Reference ${i} guide card`,
|
|
},
|
|
};
|
|
|
|
const V9_MANUAL_ONLY_WIDGETS = [
|
|
"Manual mode borrows",
|
|
"Prepare image by",
|
|
"Color kept",
|
|
"Small details kept",
|
|
"Study this image at",
|
|
"Frame this reference by",
|
|
"Subject copying",
|
|
"Early layout guidance",
|
|
"Final detail copying",
|
|
"Maximum image pull",
|
|
"Shape copied",
|
|
"Overall style reach",
|
|
];
|
|
|
|
const GUIDE_CARD_NODES = {
|
|
KGKrea2ImageGuideCardV9: {
|
|
marker: "__kgKreaV9CardUi",
|
|
patched: "__kgKreaV9CardPatched",
|
|
purposeWidget: "Use image for",
|
|
manualValue: "manual tuning",
|
|
manualOnlyWidgets: V9_MANUAL_ONLY_WIDGETS,
|
|
},
|
|
KGKrea2ImageGuideCardV10: {
|
|
marker: "__kgKreaV10CardUi",
|
|
patched: "__kgKreaV10CardPatched",
|
|
purposeWidget: "Use image for",
|
|
manualValue: "manual tuning",
|
|
// The V10 layer dials are manual-only rows; direction and timing apply
|
|
// to every purpose, so they stay enabled.
|
|
manualOnlyWidgets: [
|
|
...V9_MANUAL_ONLY_WIDGETS,
|
|
"Structure layers pull",
|
|
"Finish layers pull",
|
|
],
|
|
},
|
|
};
|
|
|
|
function getStackConfig(value) {
|
|
return STACK_NODES[value] || null;
|
|
}
|
|
|
|
function getStackNodeConfig(node) {
|
|
return getStackConfig(node?.type) || getStackConfig(node?.comfyClass);
|
|
}
|
|
|
|
function getCardConfig(value) {
|
|
return GUIDE_CARD_NODES[value] || null;
|
|
}
|
|
|
|
function getCardNodeConfig(node) {
|
|
return getCardConfig(node?.type) || getCardConfig(node?.comfyClass);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Stack encoder: compact reference-card sockets
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function isReferenceInput(input, config) {
|
|
return config.inputPattern.test(input?.name || "");
|
|
}
|
|
|
|
function inputHasLink(input) {
|
|
return input?.link != null || (Array.isArray(input?.links) && input.links.length > 0);
|
|
}
|
|
|
|
function getReferenceInputs(node) {
|
|
const config = getStackNodeConfig(node);
|
|
if (!config) return [];
|
|
return (node.inputs || [])
|
|
.map((input, index) => ({ input, index }))
|
|
.filter(({ input }) => isReferenceInput(input, config));
|
|
}
|
|
|
|
function normalizeReferenceInputs(node) {
|
|
const config = getStackNodeConfig(node);
|
|
if (!config) return;
|
|
const refs = getReferenceInputs(node);
|
|
refs.forEach(({ input }, i) => {
|
|
input.name = config.inputName(i + 1);
|
|
input.type = REFERENCE_TYPE;
|
|
});
|
|
}
|
|
|
|
function stabilizeReferenceInputs(node) {
|
|
if (!getStackNodeConfig(node)) return;
|
|
|
|
normalizeReferenceInputs(node);
|
|
let refs = getReferenceInputs(node);
|
|
|
|
let highestConnected = -1;
|
|
refs.forEach(({ input }, i) => {
|
|
if (inputHasLink(input)) highestConnected = i;
|
|
});
|
|
|
|
const desiredCount = Math.min(
|
|
MAX_REFERENCE_CARDS,
|
|
Math.max(1, highestConnected + 2)
|
|
);
|
|
|
|
for (let i = refs.length - 1; i >= desiredCount; i--) {
|
|
const { input, index } = refs[i];
|
|
if (!inputHasLink(input)) {
|
|
node.removeInput(index);
|
|
}
|
|
}
|
|
|
|
normalizeReferenceInputs(node);
|
|
refs = getReferenceInputs(node);
|
|
|
|
while (refs.length < desiredCount) {
|
|
const config = getStackNodeConfig(node);
|
|
node.addInput(config.inputName(refs.length + 1), REFERENCE_TYPE);
|
|
refs = getReferenceInputs(node);
|
|
}
|
|
|
|
normalizeReferenceInputs(node);
|
|
if (node.setDirtyCanvas) node.setDirtyCanvas(true, true);
|
|
}
|
|
|
|
function attachStackUi(node) {
|
|
const config = getStackNodeConfig(node);
|
|
if (!config || node[config.marker]) return;
|
|
node[config.marker] = true;
|
|
|
|
const previousOnConnectionsChange = node.onConnectionsChange;
|
|
node.onConnectionsChange = function () {
|
|
const result = previousOnConnectionsChange?.apply(this, arguments);
|
|
setTimeout(() => stabilizeReferenceInputs(this), 0);
|
|
return result;
|
|
};
|
|
|
|
setTimeout(() => stabilizeReferenceInputs(node), 50);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Guide card: grey out manual-only widgets outside "manual tuning"
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function findWidget(node, name) {
|
|
return (node.widgets || []).find((w) => w?.name === name) || null;
|
|
}
|
|
|
|
function refreshManualWidgetState(node) {
|
|
const config = getCardNodeConfig(node);
|
|
if (!config) return;
|
|
const purpose = findWidget(node, config.purposeWidget);
|
|
if (!purpose) return;
|
|
|
|
const manualActive = purpose.value === config.manualValue;
|
|
for (const name of config.manualOnlyWidgets) {
|
|
const widget = findWidget(node, name);
|
|
if (!widget) continue;
|
|
widget.disabled = !manualActive;
|
|
}
|
|
if (node.setDirtyCanvas) node.setDirtyCanvas(true, true);
|
|
}
|
|
|
|
function attachCardUi(node) {
|
|
const config = getCardNodeConfig(node);
|
|
if (!config || node[config.marker]) return;
|
|
node[config.marker] = true;
|
|
|
|
const purpose = findWidget(node, config.purposeWidget);
|
|
if (purpose) {
|
|
const previousCallback = purpose.callback;
|
|
purpose.callback = function () {
|
|
const result = previousCallback?.apply(this, arguments);
|
|
refreshManualWidgetState(node);
|
|
return result;
|
|
};
|
|
}
|
|
|
|
// Configure runs after widget values load from a saved workflow.
|
|
const previousOnConfigure = node.onConfigure;
|
|
node.onConfigure = function () {
|
|
const result = previousOnConfigure?.apply(this, arguments);
|
|
setTimeout(() => refreshManualWidgetState(this), 0);
|
|
return result;
|
|
};
|
|
|
|
setTimeout(() => refreshManualWidgetState(node), 50);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
app.registerExtension({
|
|
name: "kg.krea_reference_stack_v9_ui",
|
|
|
|
beforeRegisterNodeDef(nodeType, nodeData) {
|
|
const stackConfig = getStackConfig(nodeData?.name) || getStackConfig(nodeData?.comfyClass);
|
|
if (stackConfig && !nodeType[stackConfig.patched]) {
|
|
nodeType[stackConfig.patched] = true;
|
|
const previousOnCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function () {
|
|
if (previousOnCreated) previousOnCreated.apply(this, arguments);
|
|
attachStackUi(this);
|
|
};
|
|
}
|
|
|
|
const cardConfig = getCardConfig(nodeData?.name) || getCardConfig(nodeData?.comfyClass);
|
|
if (cardConfig && !nodeType[cardConfig.patched]) {
|
|
nodeType[cardConfig.patched] = true;
|
|
const previousOnCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function () {
|
|
if (previousOnCreated) previousOnCreated.apply(this, arguments);
|
|
attachCardUi(this);
|
|
};
|
|
}
|
|
},
|
|
|
|
nodeCreated(node) {
|
|
attachStackUi(node);
|
|
attachCardUi(node);
|
|
},
|
|
});
|