Download custom_nodes/dolphin_nodes/web/dolphin_lora_rows.js from bjooo/tutorials: direct link, hf CLI and curl.
- Browser
- Download file 21.4 kB
-
https://huggingface.co/bjooo/tutorials/resolve/main/custom_nodes/dolphin_nodes/web/dolphin_lora_rows.js
- Command line
-
hf download hf://bjooo/tutorials/custom_nodes/dolphin_nodes/web/dolphin_lora_rows.js
-
curl -L -o dolphin_lora_rows.js https://huggingface.co/bjooo/tutorials/resolve/main/custom_nodes/dolphin_nodes/web/dolphin_lora_rows.js
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); | |
| } | |
| }, | |
| }); | |