tutorials / custom_nodes /dolphin_nodes /web /dolphin_lora_rows.js
bjooo's picture
Upload folder using huggingface_hub
9c98083 verified
Raw History Blame Contribute Delete
21.4 kB
import { app } from "../../scripts/app.js";
// Compacts DolphinVisionDirector's 16 lora slots (each: combo + on/off + weight
// string = 3 separate stock widgets) into single power-lora-loader-style rows:
// [toggle] [lora name] [weight]. The 3 original widgets are kept exactly where
// they are in node.widgets (never reordered/removed) so saved-workflow
// widgets_values arrays keep working; they are only visually hidden (0 height).
// The compact rows are appended at the END of node.widgets via addCustomWidget,
// which never shifts the index of any pre-existing widget.
const VISION_NODE_NAME = "DolphinVisionDirector";
const ARCH_NODE_NAME = "DolphinArchitectV16";
const NUM_CLIPS = 4;
const NUM_LORAS = 4;
const NUM_ARCH_LORAS = 6;
const ROW_HEIGHT = 24;
const HEADER_HEIGHT = 18;
function fitText(ctx, str, maxWidth) {
if (!str) return "";
const full = ctx.measureText(str).width;
if (full <= maxWidth) return str;
const ellipsis = "โ€ฆ";
let lo = 0, hi = str.length;
while (lo < hi) {
const mid = Math.ceil((lo + hi) / 2);
if (ctx.measureText(str.slice(0, mid) + ellipsis).width <= maxWidth) lo = mid;
else hi = mid - 1;
}
return str.slice(0, lo) + ellipsis;
}
function hideWidget(widget, suffix = "") {
if (!widget.__dolphinHidden) {
widget.__dolphinHidden = true;
widget.origType = widget.type;
widget.origComputeSize = widget.computeSize;
widget.origSerializeValue = widget.serializeValue;
// Zero, not negative: with 48 widgets hidden at once, the usual "-4"
// margin-collapse trick (fine for a single hidden widget) compounds to a
// large negative offset that throws off LiteGraph's remaining-space
// distribution to flexible multiline widgets (essential_actions,
// artistic_vibe), ballooning their height.
widget.computeSize = () => [0, 0];
widget.type = "converted-widget" + suffix;
}
// This ComfyUI build renders stock combo/string/boolean widgets as real DOM
// elements (widget.element) absolutely positioned over the canvas, not pure
// canvas draws โ€” computeSize/type alone only affects canvas layout and does
// NOT hide the floating DOM element. Same fix the bundled tripleksampler
// custom node's own UI extension uses for its dynamic widgets.
widget.hidden = true;
if (widget.options) widget.options.hidden = true;
if (widget.element) {
widget.element.style.display = "none";
const label = widget.element.previousElementSibling;
if (label && label.textContent === widget.name + ":") {
label.style.display = "none";
}
}
}
// SERIALIZE_FALSE: every custom widget in this file is presentation-only โ€” the real
// values live on the stock widgets it wraps. LGraphNode.serialize() in this build does
// NOT call serializeValue(); it reads `widget.value` directly and writes `value ?? null`,
// skipping a widget only when `widget.serialize === false` (note: the plain property,
// not `options.serialize`). Without this flag every custom widget appended a trailing
// null to widgets_values โ€” the Architect node was saving 26 entries where 19 are real.
// The nulls were harmless (custom widgets are appended AFTER the stock ones, so no index
// shifted) but they grow every saved workflow, and old files with the extra trailing
// values still load correctly since only the tail is affected.
class DolphinSectionHeaderWidget {
constructor(label) {
this.type = "custom";
this.name = "dolphin_header_" + label;
this.__dolphinHidden = true; // never touched by hideWidget, just marks "not a real value widget"
this.serialize = false;
this.label = label;
}
computeSize(width) {
return [width, HEADER_HEIGHT];
}
draw(ctx, node, widgetWidth, posY, height) {
ctx.save();
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.globalAlpha = 0.75;
ctx.fillStyle = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR;
ctx.font = "bold 11px Arial";
ctx.fillText(this.label, 15, posY + height * 0.5);
ctx.restore();
}
mouse() {
return false;
}
serializeValue() {
return undefined;
}
}
class DolphinLoraRowWidget {
constructor(name, comboW, onW, weightW) {
this.type = "custom";
this.name = name;
this.serialize = false; // SERIALIZE_FALSE ์ฐธ๊ณ 
this.comboW = comboW;
this.onW = onW;
this.weightW = weightW;
this.hitAreas = {
toggle: [0, 0],
lora: [0, 0],
weight: [0, 0],
};
}
computeSize(width) {
return [width, ROW_HEIGHT];
}
serializeValue() {
return undefined; // real values live on comboW/onW/weightW, not on this row
}
draw(ctx, node, widgetWidth, posY, height) {
const margin = 15;
const inner = 6;
const on = this.onW.value !== false;
ctx.save();
// row background
ctx.beginPath();
ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR;
ctx.roundRect(margin, posY + 1, widgetWidth - margin * 2, height - 2, [height * 0.25]);
ctx.fill();
ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR;
ctx.stroke();
const midY = posY + height * 0.5;
let x = margin + inner;
// toggle switch
const toggleH = height - 10;
const toggleW = toggleH * 1.7;
ctx.beginPath();
ctx.fillStyle = "#00000055";
ctx.roundRect(x, posY + 5, toggleW, toggleH, [toggleH * 0.5]);
ctx.fill();
ctx.beginPath();
ctx.fillStyle = on ? "#8ab4ff" : "#777";
const knobX = on ? x + toggleW - toggleH * 0.5 : x + toggleH * 0.5;
ctx.arc(knobX, posY + height * 0.5, toggleH * 0.36, 0, Math.PI * 2);
ctx.fill();
this.hitAreas.toggle = [x, toggleW];
x += toggleW + inner * 1.5;
ctx.globalAlpha = on ? 1 : 0.45;
// weight field (measure first, right-aligned)
ctx.font = "11px Arial";
const weightText = (this.weightW.value ?? "").toString();
const weightBoxW = Math.min(100, Math.max(46, ctx.measureText(weightText).width + 14));
const weightX = widgetWidth - margin - inner - weightBoxW;
// lora name (fills remaining space)
const nameX = x;
const nameW = Math.max(20, weightX - inner - nameX);
const hasLora = this.comboW.value && this.comboW.value !== "None";
ctx.fillStyle = hasLora ? LiteGraph.WIDGET_TEXT_COLOR : LiteGraph.WIDGET_SECONDARY_TEXT_COLOR;
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillText(fitText(ctx, hasLora ? this.comboW.value : "โ€” pick lora โ€”", nameW), nameX, midY);
this.hitAreas.lora = [nameX, nameW];
// weight box
ctx.beginPath();
ctx.fillStyle = "#00000055";
ctx.roundRect(weightX, posY + 4, weightBoxW, height - 8, [4]);
ctx.fill();
ctx.fillStyle = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR;
ctx.textAlign = "center";
ctx.fillText(fitText(ctx, weightText, weightBoxW - 10), weightX + weightBoxW / 2, midY);
this.hitAreas.weight = [weightX, weightBoxW];
ctx.restore();
}
_within(pos, bounds) {
return pos[0] >= bounds[0] && pos[0] <= bounds[0] + bounds[1];
}
mouse(event, pos, node) {
if (event.type !== "pointerdown") return false;
if (this._within(pos, this.hitAreas.toggle)) {
this.onW.value = this.onW.value === false ? true : false;
node.setDirtyCanvas(true, true);
return true;
}
if (this._within(pos, this.hitAreas.lora)) {
let values = this.comboW.options?.values ?? [];
if (typeof values === "function") values = values(this.comboW, node) ?? [];
const canvas = app.canvas;
// Plain string array + single top-level callback is what makes
// LiteGraph.ContextMenu's built-in search/filter box appear (same
// pattern rgthree's showLoraChooser and KJNodes use) โ€” passing
// {content, callback} objects per item silently skips that filter.
new LiteGraph.ContextMenu([...values], {
event,
scale: Math.max(1, canvas.ds?.scale ?? 1),
className: "dark",
title: "Choose a lora",
callback: (value) => {
if (typeof value !== "string") return;
this.comboW.value = value;
if (typeof this.comboW.callback === "function") {
this.comboW.callback(value, canvas, node, pos, event);
}
node.setDirtyCanvas(true, true);
},
});
return true;
}
if (this._within(pos, this.hitAreas.weight)) {
app.canvas.prompt(
"Weight (base, high, low)",
this.weightW.value ?? "",
(v) => {
if (v !== null) {
this.weightW.value = v;
node.setDirtyCanvas(true, true);
}
},
event,
);
return true;
}
return false;
}
}
class DolphinTextFieldWidget {
constructor(label, targetW, multiline = false) {
this.type = "custom";
this.name = "dolphin_field_" + targetW.name;
this.serialize = false; // SERIALIZE_FALSE ์ฐธ๊ณ 
this.label = label;
this.targetW = targetW;
this.multiline = multiline;
this.hitAreas = { field: [0, 0] };
}
computeSize(width) {
return [width, ROW_HEIGHT];
}
serializeValue() {
return undefined; // real value lives on targetW
}
draw(ctx, node, widgetWidth, posY, height) {
const margin = 15;
const inner = 8;
ctx.save();
ctx.beginPath();
ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR;
ctx.roundRect(margin, posY + 1, widgetWidth - margin * 2, height - 2, [height * 0.25]);
ctx.fill();
ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR;
ctx.stroke();
const midY = posY + height * 0.5;
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillStyle = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR;
ctx.fillText(this.label, margin + inner, midY);
const labelW = ctx.measureText(this.label).width;
const valueX = margin + inner + labelW + inner;
const valueW = Math.max(20, widgetWidth - margin - inner - valueX);
const text = (this.targetW.value ?? "").toString().replace(/\n/g, " โŽ ");
ctx.fillStyle = text ? LiteGraph.WIDGET_TEXT_COLOR : LiteGraph.WIDGET_SECONDARY_TEXT_COLOR;
ctx.fillText(fitText(ctx, text || "(empty, click to edit)", valueW), valueX, midY);
this.hitAreas.field = [margin, widgetWidth - margin * 2];
ctx.restore();
}
_within(pos, bounds) {
return pos[0] >= bounds[0] && pos[0] <= bounds[0] + bounds[1];
}
mouse(event, pos, node) {
if (event.type !== "pointerdown") return false;
if (this._within(pos, this.hitAreas.field)) {
app.canvas.prompt(
this.label,
this.targetW.value ?? "",
(v) => {
if (v !== null) {
this.targetW.value = v;
node.setDirtyCanvas(true, true);
}
},
event,
this.multiline,
);
return true;
}
return false;
}
}
// ์ˆจ๊ฒจ์ง„ ์Šคํ†ก ์œ„์ ฏ์€ .value๋งŒ ๋ฐ”๊ฟ”๋„ ์ง๋ ฌํ™”(widgets_values)์—๋Š” ์ œ๋Œ€๋กœ ๋ฐ˜์˜๋˜์ง€๋งŒ,
// ์ด ๋นŒ๋“œ๋Š” combo/string/boolean์„ ์‹ค์ œ DOM ์—˜๋ฆฌ๋จผํŠธ๋กœ๋„ ๋“ค๊ณ  ์žˆ์–ด์„œ ๊ทธ์ชฝ ๊ฐ’์ด ๋‚จ์•„
// ์žˆ์œผ๋ฉด ๋‚˜์ค‘์— ComfyUI์˜ per-frame DOM ์‹ฑํฌ๊ฐ€ ์˜› ๊ฐ’์„ ๋˜๋Œ๋ ค ์“ธ ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ž˜์„œ
// .value๋ฅผ ๋ฐ”๊ฟ€ ๋•Œ ์—˜๋ฆฌ๋จผํŠธ๋„ ๊ฐ™์ด ๋งž์ถฐ์ค€๋‹ค.
function syncWidgetElement(widget) {
const el = widget?.element;
if (!el) return;
try {
if (el.type === "checkbox") el.checked = widget.value !== false;
else if ("value" in el) el.value = widget.value ?? "";
} catch (e) {
console.error("[Dolphin.LoraRows] element sync failed:", e);
}
}
// 6๊ฐœ ์Šฌ๋กฏ์„ Python ์ชฝ ๊ธฐ๋ณธ๊ฐ’(lora="None", on=True, weights="0.0, 0.0, 0.0")์œผ๋กœ
// ๋˜๋Œ๋ฆฌ๋Š” ๋ฒ„ํŠผ. ๋˜๋Œ๋ฆด ์ˆ˜ ์—†๋Š” ์กฐ์ž‘์ด๋ผ ContextMenu๋กœ ํ•œ ๋ฒˆ ํ™•์ธ์„ ๋ฐ›๋Š”๋‹ค -
// ํŒŒ์ผ ์ƒ๋‹จ์˜ lora chooser์™€ ๊ฐ™์€ ๋ฐฉ์‹์ด๋ผ ๋ณ„๋„ UI ์˜์กด์„ฑ์ด ์—†๋‹ค.
class DolphinResetButtonWidget {
constructor(label, slots, extraW) {
this.type = "custom";
this.name = "dolphin_reset_button";
this.serialize = false; // SERIALIZE_FALSE ์ฐธ๊ณ 
this.label = label;
this.slots = slots; // [{comboW, onW, weightW}, ...]
this.extraW = extraW; // manual_triggers (์—†์„ ์ˆ˜๋„ ์žˆ์Œ)
this.hitAreas = { button: [0, 0] };
}
computeSize(width) {
return [width, ROW_HEIGHT];
}
serializeValue() {
return undefined; // ๊ฐ’์ด ์—†๋Š” ์ˆœ์ˆ˜ ์•ก์…˜ ์œ„์ ฏ
}
draw(ctx, node, widgetWidth, posY, height) {
const margin = 15;
ctx.save();
ctx.beginPath();
ctx.fillStyle = "#00000055";
ctx.roundRect(margin, posY + 1, widgetWidth - margin * 2, height - 2, [height * 0.25]);
ctx.fill();
ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR;
ctx.stroke();
ctx.font = "11px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR;
ctx.fillText(this.label, widgetWidth * 0.5, posY + height * 0.5);
this.hitAreas.button = [margin, widgetWidth - margin * 2];
ctx.restore();
}
_within(pos, bounds) {
return pos[0] >= bounds[0] && pos[0] <= bounds[0] + bounds[1];
}
_reset(node) {
for (const { comboW, onW, weightW } of this.slots) {
comboW.value = "None";
onW.value = true;
weightW.value = "0.0, 0.0, 0.0";
syncWidgetElement(comboW);
syncWidgetElement(onW);
syncWidgetElement(weightW);
}
if (this.extraW) {
this.extraW.value = "";
syncWidgetElement(this.extraW);
}
node.setDirtyCanvas(true, true);
}
mouse(event, pos, node) {
if (event.type !== "pointerdown") return false;
if (!this._within(pos, this.hitAreas.button)) return false;
const CONFIRM = `Reset all ${this.slots.length} slots`;
new LiteGraph.ContextMenu([CONFIRM, "Cancel"], {
event,
scale: Math.max(1, app.canvas.ds?.scale ?? 1),
className: "dark",
title: "Clear every lora, weight and trigger?",
callback: (value) => {
if (value === CONFIRM) this._reset(node);
},
});
return true;
}
}
// buildRows(node) does the type-specific widget lookup + addCustomWidget calls
// and returns the array of stock widgets that got compacted into rows (so they
// can be hidden). Everything past this point (hiding, retry timers, size snap)
// is identical for every node type this gets installed on.
function installLoraRows(nodeType, buildRows) {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const ret = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
const node = this;
// Everything below is purely cosmetic (compact lora rows). If anything
// in here throws, it must NEVER propagate โ€” an uncaught error here
// during graph.configure() can abort loading the rest of the workflow,
// leaving a blank canvas. Fail loud in the console, fail safe for the user.
try {
if (!node.__dolphinLoraRowsBuilt) {
node.__dolphinLoraRowsBuilt = true;
node.__dolphinHiddenWidgets = buildRows(node) || [];
}
// widget.element (the real DOM <select>/<input>) may not exist yet at
// construction time, and ComfyUI's own per-frame DOM-sync can re-show
// it later โ€” so (re-)apply hiding now and on a couple of deferred
// passes to make sure it sticks. This does NOT touch node.size, so it
// never fights a manual resize the user makes afterwards.
const applyHiding = () => {
try {
for (const w of node.__dolphinHiddenWidgets || []) hideWidget(w);
node.setDirtyCanvas(true, true);
} catch (e) {
console.error("[Dolphin.LoraRows] applyHiding failed:", e);
}
};
applyHiding();
setTimeout(applyHiding, 50);
setTimeout(applyHiding, 250);
// Snap node.size to the freshly computed minimum ONCE on (re)build.
// Collapsing many stock widgets into compact rows frees up a lot of
// height; a stale saved node.size (from before this UI existed) would
// otherwise leave that freed space around, and flexible multiline
// widgets auto-grow to fill it.
setTimeout(() => {
try {
const computed = node.computeSize();
node.size = node.size || [0, 0];
node.size[1] = computed[1];
node.setDirtyCanvas(true, true);
} catch (e) {
console.error("[Dolphin.LoraRows] size snap failed:", e);
}
}, 300);
} catch (e) {
console.error("[Dolphin.LoraRows] onNodeCreated hook failed:", e);
}
return ret;
};
}
function buildVisionDirectorRows(node) {
const hidden = [];
for (let c = 1; c <= NUM_CLIPS; c++) {
node.addCustomWidget(new DolphinSectionHeaderWidget(`โ”€โ”€ Clip ${c} Loras โ”€โ”€`));
for (let n = 1; n <= NUM_LORAS; n++) {
const comboW = node.widgets.find((w) => w.name === `c${c}_lora_${n}`);
const onW = node.widgets.find((w) => w.name === `c${c}_lora_${n}_on`);
const weightW = node.widgets.find((w) => w.name === `c${c}_w_${n}`);
if (!comboW || !onW || !weightW) continue;
hidden.push(comboW, onW, weightW);
node.addCustomWidget(new DolphinLoraRowWidget(`c${c}_lora_${n}_row`, comboW, onW, weightW));
}
}
const manualTriggersW = node.widgets.find((w) => w.name === "manual_triggers");
if (manualTriggersW) {
hidden.push(manualTriggersW);
node.addCustomWidget(new DolphinTextFieldWidget("manual_triggers:", manualTriggersW, true));
}
return hidden;
}
// DolphinArchitectV16 (๐Ÿฌ Dolphin Architect V16): one flat bank of lora_1..6 /
// lora_1_on..6_on / weights_1..6 (each already base+high+low as one comma
// string, e.g. "0.4, 0.7, 0") โ€” no per-clip grouping, so no section headers.
// ํŒŒ์ผ๋ช…์— HIGH/LOW๊ฐ€ ๋“ค์–ด์žˆ์œผ๋ฉด Wan2.2 ๋ถ„๋ฆฌ ๋กœ๋ผ๋กœ ๋ณด๊ณ  ๊ทธ์ชฝ ๋ชจ๋ธ ์Šฌ๋กฏ๋งŒ ์ผ ๋‹ค
// (base,high,low). ๋‘˜ ๋‹ค ์—†์œผ๋ฉด Wan2.1 ๋“ฑ ๋ฒ”์šฉ ๋กœ๋ผ๋กœ ๋ณด๊ณ  ์…‹ ๋‹ค ์ผ ๋‹ค. "high_noise"/
// "low_noise" ๊ฐ™์€ ๋ณ€ํ˜•๋„ ๊ทธ๋ƒฅ substring ๋งค์น˜๋ผ ์ž๋™์œผ๋กœ ๊ฑธ๋ฆฐ๋‹ค.
// HN/LN ์•ฝ์–ด(-HN-, _HN_, ...-HN.safetensors ์‹)๋Š” ์•ž๋’ค๊ฐ€ ์˜์ˆซ์ž๊ฐ€ ์•„๋‹ ๋•Œ๋งŒ ๋งค์น˜ํ•˜๋Š”
// ํ† ํฐ ๊ฒฝ๊ณ„ ์ •๊ทœ์‹์„ ์“ด๋‹ค - ๊ทธ๋ƒฅ substring์ด๋ฉด "krea2DepthControlnet"(contro-L-N-et)
// ๊ฐ™์€ ๋ฌด๊ด€ํ•œ ํŒŒ์ผ๋ช…์ด "ln"์„ ์šฐ์—ฐํžˆ ํฌํ•จํ•ด์„œ Low Noise๋กœ ์˜ค์ธ์‹๋œ๋‹ค.
const HN_TOKEN = /(?:^|[^a-z0-9])hn(?:[^a-z0-9]|$)/;
const LN_TOKEN = /(?:^|[^a-z0-9])ln(?:[^a-z0-9]|$)/;
// ํ•œ ๊ธ€์ž H/L ํ† ํฐ(-H-, _H_, -L-, _L_, ๊ทธ๋ฆฌ๊ณ  ํ™•์žฅ์ž/์ด๋ฆ„ ๋์˜ _Hยท-L ๋“ฑ).
// hn/ln๋ณด๋‹ค ํ•œ ๊ธ€์ž ์งง์•„์„œ ์˜คํƒ ์œ„ํ—˜์ด ํ›จ์”ฌ ํฌ๊ธฐ ๋•Œ๋ฌธ์— ๋‘ ๊ฐ€์ง€๋กœ ์ขํ˜”๋‹ค:
// (1) ๊ตฌ๋ถ„์ž๋ฅผ '-'์™€ '_'๋กœ๋งŒ ํ•œ์ •ํ•œ๋‹ค. hn/ln์ด ์“ฐ๋Š” ์ผ๋ฐ˜ ํ† ํฐ ๊ฒฝ๊ณ„([^a-z0-9])๊นŒ์ง€
// ํ—ˆ์šฉํ•˜๋ฉด "flaccid-v1.0"์˜ ์ ยท๊ณต๋ฐฑยท๊ด„ํ˜ธ ๊ฐ™์€ ๋ฌธ์ž๋„ ๊ฒฝ๊ณ„๊ฐ€ ๋˜์–ด ๋ฒ”์œ„๊ฐ€ ๊ธ‰๊ฒฉํžˆ ๋„“์–ด์ง„๋‹ค.
// (2) ๋Œ€๋ฌธ์ž๋งŒ ๋งค์น˜ํ•œ๋‹ค(๊ทธ๋ž˜์„œ ์†Œ๋ฌธ์žํ™”ํ•œ n์ด ์•„๋‹ˆ๋ผ ์›๋ณธ raw๋กœ ๊ฒ€์‚ฌ). HIGH/LOW ์•ฝ์–ด๋Š”
// ๊ด€๋ก€์ ์œผ๋กœ ๋Œ€๋ฌธ์ž์ด๊ณ , ์†Œ๋ฌธ์ž๊นŒ์ง€ ๋ฐ›์œผ๋ฉด "..._v2_h_final" ๊ฐ™์€ ๋ฌด๊ด€ํ•œ ์ด๋ฆ„์ด ๊ฑธ๋ฆฐ๋‹ค.
const H_TOKEN = /[-_]H(?=[-_]|\.[A-Za-z0-9]+$|$)/;
const L_TOKEN = /[-_]L(?=[-_]|\.[A-Za-z0-9]+$|$)/;
function guessLoraWeights(name) {
if (!name || name === "None") return null;
const raw = String(name);
const n = raw.toLowerCase();
if (n.includes("high") || HN_TOKEN.test(n) || H_TOKEN.test(raw)) return "1, 1, 0";
if (n.includes("low") || LN_TOKEN.test(n) || L_TOKEN.test(raw)) return "0, 0, 1";
return "1, 1, 1";
}
// comboW.callback์€ ์œ ์ €๊ฐ€ ๋“œ๋กญ๋‹ค์šด/ํ–‰-UI์—์„œ ์‹ค์ œ๋กœ ์„ ํƒํ–ˆ์„ ๋•Œ๋งŒ ๋ถˆ๋ฆฐ๋‹ค - ์›Œํฌํ”Œ๋กœ์šฐ
// ๋กœ๋“œ ์‹œ configure()๊ฐ€ widgets_values๋ฅผ ๋ณต์›ํ•˜๋Š” ๊ฒฝ๋กœ๋Š” .value๋ฅผ ์ง์ ‘ ๋Œ€์ž…ํ•  ๋ฟ ์ด
// callback์„ ๊ฑฐ์น˜์ง€ ์•Š์œผ๋ฏ€๋กœ, ์ €์žฅ๋œ ์ˆ˜๋™ ๊ฐ€์ค‘์น˜๋ฅผ ์ž๋™์œผ๋กœ ๋ฎ์–ด์“ธ ์œ„ํ—˜์ด ์—†๋‹ค.
function wireAutoWeight(comboW, weightW) {
const orig = comboW.callback;
comboW.callback = function (value, ...rest) {
const guess = guessLoraWeights(value);
if (guess !== null) weightW.value = guess;
return orig ? orig.apply(this, [value, ...rest]) : undefined;
};
}
function buildArchitectRows(node) {
const hidden = [];
const slots = [];
for (let n = 1; n <= NUM_ARCH_LORAS; n++) {
const comboW = node.widgets.find((w) => w.name === `lora_${n}`);
const onW = node.widgets.find((w) => w.name === `lora_${n}_on`);
const weightW = node.widgets.find((w) => w.name === `weights_${n}`);
if (!comboW || !onW || !weightW) continue;
wireAutoWeight(comboW, weightW);
hidden.push(comboW, onW, weightW);
slots.push({ comboW, onW, weightW });
node.addCustomWidget(new DolphinLoraRowWidget(`lora_${n}_row`, comboW, onW, weightW));
}
const manualTriggersW = node.widgets.find((w) => w.name === "manual_triggers");
if (manualTriggersW) {
hidden.push(manualTriggersW);
node.addCustomWidget(new DolphinTextFieldWidget("manual_triggers:", manualTriggersW, true));
}
if (slots.length) {
node.addCustomWidget(new DolphinResetButtonWidget("โŸฒ Reset all slots", slots, manualTriggersW));
}
return hidden;
}
app.registerExtension({
name: "Dolphin.LoraRows",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData?.name === VISION_NODE_NAME) {
installLoraRows(nodeType, buildVisionDirectorRows);
} else if (nodeData?.name === ARCH_NODE_NAME) {
installLoraRows(nodeType, buildArchitectRows);
}
},
});