mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 07:05:43 -03:00
@@ -1,10 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from nodes import LoraLoader
|
from nodes import LoraLoader
|
||||||
from comfy.comfy_types import IO # type: ignore
|
from comfy.comfy_types import IO # type: ignore
|
||||||
from ..services.lora_scanner import LoraScanner
|
|
||||||
from ..config import config
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
|
||||||
from .utils import FlexibleOptionalInputType, any_type, get_lora_info, extract_lora_name, get_loras_list
|
from .utils import FlexibleOptionalInputType, any_type, get_lora_info, extract_lora_name, get_loras_list
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -51,23 +48,35 @@ class LoraManagerLoader:
|
|||||||
_, trigger_words = asyncio.run(get_lora_info(lora_name))
|
_, trigger_words = asyncio.run(get_lora_info(lora_name))
|
||||||
|
|
||||||
all_trigger_words.extend(trigger_words)
|
all_trigger_words.extend(trigger_words)
|
||||||
loaded_loras.append(f"{lora_name}: {model_strength}")
|
# Add clip strength to output if different from model strength
|
||||||
|
if abs(model_strength - clip_strength) > 0.001:
|
||||||
|
loaded_loras.append(f"{lora_name}: {model_strength},{clip_strength}")
|
||||||
|
else:
|
||||||
|
loaded_loras.append(f"{lora_name}: {model_strength}")
|
||||||
|
|
||||||
# Then process loras from kwargs with support for both old and new formats
|
# Then process loras from kwargs with support for both old and new formats
|
||||||
loras_list = get_loras_list(kwargs)
|
loras_list = get_loras_list(kwargs)
|
||||||
|
print(f"Loaded loras list: {loras_list}")
|
||||||
for lora in loras_list:
|
for lora in loras_list:
|
||||||
if not lora.get('active', False):
|
if not lora.get('active', False):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
lora_name = lora['name']
|
lora_name = lora['name']
|
||||||
strength = float(lora['strength'])
|
model_strength = float(lora['strength'])
|
||||||
|
# Get clip strength - use model strength as default if not specified
|
||||||
|
clip_strength = float(lora.get('clipStrength', model_strength))
|
||||||
|
|
||||||
# Get lora path and trigger words
|
# Get lora path and trigger words
|
||||||
lora_path, trigger_words = asyncio.run(get_lora_info(lora_name))
|
lora_path, trigger_words = asyncio.run(get_lora_info(lora_name))
|
||||||
|
|
||||||
# Apply the LoRA using the resolved path
|
# Apply the LoRA using the resolved path with separate strengths
|
||||||
model, clip = LoraLoader().load_lora(model, clip, lora_path, strength, strength)
|
model, clip = LoraLoader().load_lora(model, clip, lora_path, model_strength, clip_strength)
|
||||||
loaded_loras.append(f"{lora_name}: {strength}")
|
|
||||||
|
# Include clip strength in output if different from model strength
|
||||||
|
if abs(model_strength - clip_strength) > 0.001:
|
||||||
|
loaded_loras.append(f"{lora_name}: {model_strength},{clip_strength}")
|
||||||
|
else:
|
||||||
|
loaded_loras.append(f"{lora_name}: {model_strength}")
|
||||||
|
|
||||||
# Add trigger words to collection
|
# Add trigger words to collection
|
||||||
all_trigger_words.extend(trigger_words)
|
all_trigger_words.extend(trigger_words)
|
||||||
@@ -75,8 +84,23 @@ class LoraManagerLoader:
|
|||||||
# use ',, ' to separate trigger words for group mode
|
# use ',, ' to separate trigger words for group mode
|
||||||
trigger_words_text = ",, ".join(all_trigger_words) if all_trigger_words else ""
|
trigger_words_text = ",, ".join(all_trigger_words) if all_trigger_words else ""
|
||||||
|
|
||||||
# Format loaded_loras as <lora:lora_name:strength> separated by spaces
|
# Format loaded_loras with support for both formats
|
||||||
formatted_loras = " ".join([f"<lora:{name.split(':')[0].strip()}:{str(strength).strip()}>"
|
formatted_loras = []
|
||||||
for name, strength in [item.split(':') for item in loaded_loras]])
|
for item in loaded_loras:
|
||||||
|
parts = item.split(":")
|
||||||
|
lora_name = parts[0].strip()
|
||||||
|
strength_parts = parts[1].strip().split(",")
|
||||||
|
|
||||||
|
if len(strength_parts) > 1:
|
||||||
|
# Different model and clip strengths
|
||||||
|
model_str = strength_parts[0].strip()
|
||||||
|
clip_str = strength_parts[1].strip()
|
||||||
|
formatted_loras.append(f"<lora:{lora_name}:{model_str}:{clip_str}>")
|
||||||
|
else:
|
||||||
|
# Same strength for both
|
||||||
|
model_str = strength_parts[0].strip()
|
||||||
|
formatted_loras.append(f"<lora:{lora_name}:{model_str}>")
|
||||||
|
|
||||||
|
formatted_loras_text = " ".join(formatted_loras)
|
||||||
|
|
||||||
return (model, clip, trigger_words_text, formatted_loras)
|
return (model, clip, trigger_words_text, formatted_loras_text)
|
||||||
@@ -38,7 +38,7 @@ class LoraStacker:
|
|||||||
|
|
||||||
# Process existing lora_stack if available
|
# Process existing lora_stack if available
|
||||||
lora_stack = kwargs.get('lora_stack', None)
|
lora_stack = kwargs.get('lora_stack', None)
|
||||||
if lora_stack:
|
if (lora_stack):
|
||||||
stack.extend(lora_stack)
|
stack.extend(lora_stack)
|
||||||
# Get trigger words from existing stack entries
|
# Get trigger words from existing stack entries
|
||||||
for lora_path, _, _ in lora_stack:
|
for lora_path, _, _ in lora_stack:
|
||||||
@@ -54,7 +54,8 @@ class LoraStacker:
|
|||||||
|
|
||||||
lora_name = lora['name']
|
lora_name = lora['name']
|
||||||
model_strength = float(lora['strength'])
|
model_strength = float(lora['strength'])
|
||||||
clip_strength = model_strength # Using same strength for both as in the original loader
|
# Get clip strength - use model strength as default if not specified
|
||||||
|
clip_strength = float(lora.get('clipStrength', model_strength))
|
||||||
|
|
||||||
# Get lora path and trigger words
|
# Get lora path and trigger words
|
||||||
lora_path, trigger_words = asyncio.run(get_lora_info(lora_name))
|
lora_path, trigger_words = asyncio.run(get_lora_info(lora_name))
|
||||||
@@ -62,15 +63,24 @@ class LoraStacker:
|
|||||||
# Add to stack without loading
|
# Add to stack without loading
|
||||||
# replace '/' with os.sep to avoid different OS path format
|
# replace '/' with os.sep to avoid different OS path format
|
||||||
stack.append((lora_path.replace('/', os.sep), model_strength, clip_strength))
|
stack.append((lora_path.replace('/', os.sep), model_strength, clip_strength))
|
||||||
active_loras.append((lora_name, model_strength))
|
active_loras.append((lora_name, model_strength, clip_strength))
|
||||||
|
|
||||||
# Add trigger words to collection
|
# Add trigger words to collection
|
||||||
all_trigger_words.extend(trigger_words)
|
all_trigger_words.extend(trigger_words)
|
||||||
|
|
||||||
# use ',, ' to separate trigger words for group mode
|
# use ',, ' to separate trigger words for group mode
|
||||||
trigger_words_text = ",, ".join(all_trigger_words) if all_trigger_words else ""
|
trigger_words_text = ",, ".join(all_trigger_words) if all_trigger_words else ""
|
||||||
# Format active_loras as <lora:lora_name:strength> separated by spaces
|
|
||||||
active_loras_text = " ".join([f"<lora:{name}:{str(strength).strip()}>"
|
# Format active_loras with support for both formats
|
||||||
for name, strength in active_loras])
|
formatted_loras = []
|
||||||
|
for name, model_strength, clip_strength in active_loras:
|
||||||
|
if abs(model_strength - clip_strength) > 0.001:
|
||||||
|
# Different model and clip strengths
|
||||||
|
formatted_loras.append(f"<lora:{name}:{str(model_strength).strip()}:{str(clip_strength).strip()}>")
|
||||||
|
else:
|
||||||
|
# Same strength for both
|
||||||
|
formatted_loras.append(f"<lora:{name}:{str(model_strength).strip()}>")
|
||||||
|
|
||||||
|
active_loras_text = " ".join(formatted_loras)
|
||||||
|
|
||||||
return (stack, trigger_words_text, active_loras_text)
|
return (stack, trigger_words_text, active_loras_text)
|
||||||
|
|||||||
@@ -880,29 +880,3 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove the old duplicate styles that are no longer needed */
|
|
||||||
.duplicate-recipe-item,
|
|
||||||
.duplicate-recipe-content,
|
|
||||||
.duplicate-recipe-actions,
|
|
||||||
.danger-btn,
|
|
||||||
.view-recipe-btn {
|
|
||||||
/* These styles are being replaced by the card layout */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal buttons layout to accommodate multiple buttons */
|
|
||||||
.modal-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-actions button {
|
|
||||||
flex: 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { app } from "../../scripts/app.js";
|
import { app } from "../../scripts/app.js";
|
||||||
import { dynamicImportByVersion } from "./utils.js";
|
import { dynamicImportByVersion } from "./utils.js";
|
||||||
|
|
||||||
// Extract pattern into a constant for consistent use
|
// Update pattern to match both formats: <lora:name:model_strength> or <lora:name:model_strength:clip_strength>
|
||||||
const LORA_PATTERN = /<lora:([^:]+):([-\d\.]+)>/g;
|
const LORA_PATTERN = /<lora:([^:]+):([-\d\.]+)(?::([-\d\.]+))?>/g;
|
||||||
|
|
||||||
// Function to get the appropriate loras widget based on ComfyUI version
|
// Function to get the appropriate loras widget based on ComfyUI version
|
||||||
async function getLorasWidgetModule() {
|
async function getLorasWidgetModule() {
|
||||||
@@ -61,10 +61,15 @@ function mergeLoras(lorasText, lorasArr) {
|
|||||||
const result = [];
|
const result = [];
|
||||||
let match;
|
let match;
|
||||||
|
|
||||||
|
// Reset pattern index before using
|
||||||
|
LORA_PATTERN.lastIndex = 0;
|
||||||
|
|
||||||
// Parse text input and create initial entries
|
// Parse text input and create initial entries
|
||||||
while ((match = LORA_PATTERN.exec(lorasText)) !== null) {
|
while ((match = LORA_PATTERN.exec(lorasText)) !== null) {
|
||||||
const name = match[1];
|
const name = match[1];
|
||||||
const inputStrength = Number(match[2]);
|
const modelStrength = Number(match[2]);
|
||||||
|
// Extract clip strength if provided, otherwise use model strength
|
||||||
|
const clipStrength = match[3] ? Number(match[3]) : modelStrength;
|
||||||
|
|
||||||
// Find if this lora exists in the array data
|
// Find if this lora exists in the array data
|
||||||
const existingLora = lorasArr.find(l => l.name === name);
|
const existingLora = lorasArr.find(l => l.name === name);
|
||||||
@@ -72,8 +77,9 @@ function mergeLoras(lorasText, lorasArr) {
|
|||||||
result.push({
|
result.push({
|
||||||
name: name,
|
name: name,
|
||||||
// Use existing strength if available, otherwise use input strength
|
// Use existing strength if available, otherwise use input strength
|
||||||
strength: existingLora ? existingLora.strength : inputStrength,
|
strength: existingLora ? existingLora.strength : modelStrength,
|
||||||
active: existingLora ? existingLora.active : true
|
active: existingLora ? existingLora.active : true,
|
||||||
|
clipStrength: existingLora ? existingLora.clipStrength : clipStrength,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,18 +108,7 @@ app.registerExtension({
|
|||||||
let existingLoras = [];
|
let existingLoras = [];
|
||||||
if (node.widgets_values && node.widgets_values.length > 0) {
|
if (node.widgets_values && node.widgets_values.length > 0) {
|
||||||
const savedValue = node.widgets_values[1];
|
const savedValue = node.widgets_values[1];
|
||||||
// TODO: clean up this code
|
existingLoras = savedValue || [];
|
||||||
try {
|
|
||||||
// Check if the value is already an array/object
|
|
||||||
if (typeof savedValue === 'object' && savedValue !== null) {
|
|
||||||
existingLoras = savedValue;
|
|
||||||
} else if (typeof savedValue === 'string') {
|
|
||||||
existingLoras = JSON.parse(savedValue);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Failed to parse loras data:", e);
|
|
||||||
existingLoras = [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Merge the loras data
|
// Merge the loras data
|
||||||
const mergedLoras = mergeLoras(node.widgets[0].value, existingLoras);
|
const mergedLoras = mergeLoras(node.widgets[0].value, existingLoras);
|
||||||
@@ -139,7 +134,7 @@ app.registerExtension({
|
|||||||
const currentLoras = value.map(l => l.name);
|
const currentLoras = value.map(l => l.name);
|
||||||
|
|
||||||
// Use the constant pattern here as well
|
// Use the constant pattern here as well
|
||||||
let newText = inputWidget.value.replace(LORA_PATTERN, (match, name, strength) => {
|
let newText = inputWidget.value.replace(LORA_PATTERN, (match, name, strength, clipStrength) => {
|
||||||
return currentLoras.includes(name) ? match : '';
|
return currentLoras.includes(name) ? match : '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { app } from "../../scripts/app.js";
|
import { app } from "../../scripts/app.js";
|
||||||
import { dynamicImportByVersion } from "./utils.js";
|
import { dynamicImportByVersion } from "./utils.js";
|
||||||
|
|
||||||
// Extract pattern into a constant for consistent use
|
// Update pattern to match both formats: <lora:name:model_strength> or <lora:name:model_strength:clip_strength>
|
||||||
const LORA_PATTERN = /<lora:([^:]+):([-\d\.]+)>/g;
|
const LORA_PATTERN = /<lora:([^:]+):([-\d\.]+)(?::([-\d\.]+))?>/g;
|
||||||
|
|
||||||
// Function to get the appropriate loras widget based on ComfyUI version
|
// Function to get the appropriate loras widget based on ComfyUI version
|
||||||
async function getLorasWidgetModule() {
|
async function getLorasWidgetModule() {
|
||||||
@@ -57,10 +57,15 @@ function mergeLoras(lorasText, lorasArr) {
|
|||||||
const result = [];
|
const result = [];
|
||||||
let match;
|
let match;
|
||||||
|
|
||||||
|
// Reset pattern index before using
|
||||||
|
LORA_PATTERN.lastIndex = 0;
|
||||||
|
|
||||||
// Parse text input and create initial entries
|
// Parse text input and create initial entries
|
||||||
while ((match = LORA_PATTERN.exec(lorasText)) !== null) {
|
while ((match = LORA_PATTERN.exec(lorasText)) !== null) {
|
||||||
const name = match[1];
|
const name = match[1];
|
||||||
const inputStrength = Number(match[2]);
|
const modelStrength = Number(match[2]);
|
||||||
|
// Extract clip strength if provided, otherwise use model strength
|
||||||
|
const clipStrength = match[3] ? Number(match[3]) : modelStrength;
|
||||||
|
|
||||||
// Find if this lora exists in the array data
|
// Find if this lora exists in the array data
|
||||||
const existingLora = lorasArr.find(l => l.name === name);
|
const existingLora = lorasArr.find(l => l.name === name);
|
||||||
@@ -68,8 +73,9 @@ function mergeLoras(lorasText, lorasArr) {
|
|||||||
result.push({
|
result.push({
|
||||||
name: name,
|
name: name,
|
||||||
// Use existing strength if available, otherwise use input strength
|
// Use existing strength if available, otherwise use input strength
|
||||||
strength: existingLora ? existingLora.strength : inputStrength,
|
strength: existingLora ? existingLora.strength : modelStrength,
|
||||||
active: existingLora ? existingLora.active : true
|
active: existingLora ? existingLora.active : true,
|
||||||
|
clipStrength: existingLora ? existingLora.clipStrength : clipStrength,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,10 +29,13 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
|
|
||||||
// Fixed sizes for component calculations
|
// Fixed sizes for component calculations
|
||||||
const LORA_ENTRY_HEIGHT = 40; // Height of a single lora entry
|
const LORA_ENTRY_HEIGHT = 40; // Height of a single lora entry
|
||||||
|
const CLIP_ENTRY_HEIGHT = 40; // Height of a clip entry
|
||||||
const HEADER_HEIGHT = 40; // Height of the header section
|
const HEADER_HEIGHT = 40; // Height of the header section
|
||||||
const CONTAINER_PADDING = 12; // Top and bottom padding
|
const CONTAINER_PADDING = 12; // Top and bottom padding
|
||||||
const EMPTY_CONTAINER_HEIGHT = 100; // Height when no loras are present
|
const EMPTY_CONTAINER_HEIGHT = 100; // Height when no loras are present
|
||||||
|
|
||||||
|
// Remove expandedClipEntries Set since we'll determine expansion based on strength values
|
||||||
|
|
||||||
// Parse LoRA entries from value
|
// Parse LoRA entries from value
|
||||||
const parseLoraValue = (value) => {
|
const parseLoraValue = (value) => {
|
||||||
if (!value) return [];
|
if (!value) return [];
|
||||||
@@ -367,7 +370,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Function to handle strength adjustment via dragging
|
// Function to handle strength adjustment via dragging
|
||||||
const handleStrengthDrag = (name, initialStrength, initialX, event, widget) => {
|
const handleStrengthDrag = (name, initialStrength, initialX, event, widget, isClipStrength = false) => {
|
||||||
// Calculate drag sensitivity (how much the strength changes per pixel)
|
// Calculate drag sensitivity (how much the strength changes per pixel)
|
||||||
// Using 0.01 per 10 pixels of movement
|
// Using 0.01 per 10 pixels of movement
|
||||||
const sensitivity = 0.001;
|
const sensitivity = 0.001;
|
||||||
@@ -391,7 +394,12 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
const loraIndex = lorasData.findIndex(l => l.name === name);
|
const loraIndex = lorasData.findIndex(l => l.name === name);
|
||||||
|
|
||||||
if (loraIndex >= 0) {
|
if (loraIndex >= 0) {
|
||||||
lorasData[loraIndex].strength = newStrength;
|
// Update the appropriate strength property based on isClipStrength flag
|
||||||
|
if (isClipStrength) {
|
||||||
|
lorasData[loraIndex].clipStrength = newStrength;
|
||||||
|
} else {
|
||||||
|
lorasData[loraIndex].strength = newStrength;
|
||||||
|
}
|
||||||
|
|
||||||
// Update the widget value
|
// Update the widget value
|
||||||
widget.value = formatLoraValue(lorasData);
|
widget.value = formatLoraValue(lorasData);
|
||||||
@@ -402,7 +410,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Function to initialize drag operation
|
// Function to initialize drag operation
|
||||||
const initDrag = (loraEl, nameEl, name, widget) => {
|
const initDrag = (dragEl, name, widget, isClipStrength = false) => {
|
||||||
let isDragging = false;
|
let isDragging = false;
|
||||||
let initialX = 0;
|
let initialX = 0;
|
||||||
let initialStrength = 0;
|
let initialStrength = 0;
|
||||||
@@ -420,9 +428,8 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
document.head.appendChild(styleEl);
|
document.head.appendChild(styleEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a drag handler that's applied to the entire lora entry
|
// Create a drag handler
|
||||||
// except toggle and strength controls
|
dragEl.addEventListener('mousedown', (e) => {
|
||||||
loraEl.addEventListener('mousedown', (e) => {
|
|
||||||
// Skip if clicking on toggle or strength control areas
|
// Skip if clicking on toggle or strength control areas
|
||||||
if (e.target.closest('.comfy-lora-toggle') ||
|
if (e.target.closest('.comfy-lora-toggle') ||
|
||||||
e.target.closest('input') ||
|
e.target.closest('input') ||
|
||||||
@@ -437,7 +444,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
if (!loraData) return;
|
if (!loraData) return;
|
||||||
|
|
||||||
initialX = e.clientX;
|
initialX = e.clientX;
|
||||||
initialStrength = loraData.strength;
|
initialStrength = isClipStrength ? loraData.clipStrength : loraData.strength;
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
|
|
||||||
// Add class to body to enforce cursor style globally
|
// Add class to body to enforce cursor style globally
|
||||||
@@ -453,7 +460,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
if (!isDragging) return;
|
if (!isDragging) return;
|
||||||
|
|
||||||
// Call the strength adjustment function
|
// Call the strength adjustment function
|
||||||
handleStrengthDrag(name, initialStrength, initialX, e, widget);
|
handleStrengthDrag(name, initialStrength, initialX, e, widget, isClipStrength);
|
||||||
|
|
||||||
// Prevent showing the preview tooltip during drag
|
// Prevent showing the preview tooltip during drag
|
||||||
previewTooltip.hide();
|
previewTooltip.hide();
|
||||||
@@ -691,10 +698,17 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
header.appendChild(strengthLabel);
|
header.appendChild(strengthLabel);
|
||||||
container.appendChild(header);
|
container.appendChild(header);
|
||||||
|
|
||||||
|
// Track the total visible entries for height calculation
|
||||||
|
let totalVisibleEntries = lorasData.length;
|
||||||
|
|
||||||
// Render each lora entry
|
// Render each lora entry
|
||||||
lorasData.forEach((loraData) => {
|
lorasData.forEach((loraData) => {
|
||||||
const { name, strength, active } = loraData;
|
const { name, strength, clipStrength, active } = loraData;
|
||||||
|
|
||||||
|
// Determine expansion state using our helper function
|
||||||
|
const isExpanded = shouldShowClipEntry(loraData);
|
||||||
|
|
||||||
|
// Create the main LoRA entry
|
||||||
const loraEl = document.createElement("div");
|
const loraEl = document.createElement("div");
|
||||||
loraEl.className = "comfy-lora-entry";
|
loraEl.className = "comfy-lora-entry";
|
||||||
Object.assign(loraEl.style, {
|
Object.assign(loraEl.style, {
|
||||||
@@ -708,6 +722,41 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
marginBottom: "4px",
|
marginBottom: "4px",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add double-click handler to toggle clip entry
|
||||||
|
loraEl.addEventListener('dblclick', (e) => {
|
||||||
|
// Skip if clicking on toggle or strength control areas
|
||||||
|
if (e.target.closest('.comfy-lora-toggle') ||
|
||||||
|
e.target.closest('input') ||
|
||||||
|
e.target.closest('.comfy-lora-arrow')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent default behavior
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// Toggle the clip entry expanded state
|
||||||
|
const lorasData = parseLoraValue(widget.value);
|
||||||
|
const loraIndex = lorasData.findIndex(l => l.name === name);
|
||||||
|
|
||||||
|
if (loraIndex >= 0) {
|
||||||
|
// Explicitly toggle the expansion state
|
||||||
|
const currentExpanded = shouldShowClipEntry(lorasData[loraIndex]);
|
||||||
|
lorasData[loraIndex].expanded = !currentExpanded;
|
||||||
|
|
||||||
|
// If collapsing, set clipStrength = strength
|
||||||
|
if (!lorasData[loraIndex].expanded) {
|
||||||
|
lorasData[loraIndex].clipStrength = lorasData[loraIndex].strength;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the widget value
|
||||||
|
widget.value = formatLoraValue(lorasData);
|
||||||
|
|
||||||
|
// Re-render to show/hide clip entry
|
||||||
|
renderLoras(widget.value, widget);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Create toggle for this lora
|
// Create toggle for this lora
|
||||||
const toggle = createToggle(active, (newActive) => {
|
const toggle = createToggle(active, (newActive) => {
|
||||||
// Update this lora's active state
|
// Update this lora's active state
|
||||||
@@ -740,6 +789,14 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
msUserSelect: "none",
|
msUserSelect: "none",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add expand indicator to name element
|
||||||
|
const expandIndicator = document.createElement("span");
|
||||||
|
expandIndicator.textContent = isExpanded ? " ▼" : " ▶";
|
||||||
|
expandIndicator.style.opacity = "0.7";
|
||||||
|
expandIndicator.style.fontSize = "9px";
|
||||||
|
expandIndicator.style.marginLeft = "4px";
|
||||||
|
nameEl.appendChild(expandIndicator);
|
||||||
|
|
||||||
// Move preview tooltip events to nameEl instead of loraEl
|
// Move preview tooltip events to nameEl instead of loraEl
|
||||||
nameEl.addEventListener('mouseenter', async (e) => {
|
nameEl.addEventListener('mouseenter', async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -753,7 +810,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Initialize drag functionality for strength adjustment
|
// Initialize drag functionality for strength adjustment
|
||||||
initDrag(loraEl, nameEl, name, widget);
|
initDrag(loraEl, name, widget, false);
|
||||||
|
|
||||||
// Remove the preview tooltip events from loraEl
|
// Remove the preview tooltip events from loraEl
|
||||||
loraEl.onmouseenter = () => {
|
loraEl.onmouseenter = () => {
|
||||||
@@ -897,10 +954,185 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
loraEl.appendChild(strengthControl);
|
loraEl.appendChild(strengthControl);
|
||||||
|
|
||||||
container.appendChild(loraEl);
|
container.appendChild(loraEl);
|
||||||
|
|
||||||
|
// If expanded, show the clip entry
|
||||||
|
if (isExpanded) {
|
||||||
|
totalVisibleEntries++;
|
||||||
|
const clipEl = document.createElement("div");
|
||||||
|
clipEl.className = "comfy-lora-clip-entry";
|
||||||
|
Object.assign(clipEl.style, {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "6px",
|
||||||
|
paddingLeft: "20px", // Indent to align with parent name
|
||||||
|
borderRadius: "6px",
|
||||||
|
backgroundColor: active ? "rgba(65, 70, 90, 0.6)" : "rgba(50, 55, 65, 0.5)",
|
||||||
|
borderLeft: "2px solid rgba(72, 118, 255, 0.6)",
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
marginBottom: "4px",
|
||||||
|
marginLeft: "10px",
|
||||||
|
marginTop: "-2px"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create clip name display
|
||||||
|
const clipNameEl = document.createElement("div");
|
||||||
|
clipNameEl.textContent = "[clip] " + name;
|
||||||
|
Object.assign(clipNameEl.style, {
|
||||||
|
flex: "1",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
color: active ? "rgba(200, 215, 240, 0.9)" : "rgba(200, 215, 240, 0.6)",
|
||||||
|
fontSize: "13px",
|
||||||
|
userSelect: "none",
|
||||||
|
WebkitUserSelect: "none",
|
||||||
|
MozUserSelect: "none",
|
||||||
|
msUserSelect: "none",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create clip strength control
|
||||||
|
const clipStrengthControl = document.createElement("div");
|
||||||
|
Object.assign(clipStrengthControl.style, {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "8px",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Left arrow for clip
|
||||||
|
const clipLeftArrow = createArrowButton("left", () => {
|
||||||
|
// Decrease clip strength
|
||||||
|
const lorasData = parseLoraValue(widget.value);
|
||||||
|
const loraIndex = lorasData.findIndex(l => l.name === name);
|
||||||
|
|
||||||
|
if (loraIndex >= 0) {
|
||||||
|
lorasData[loraIndex].clipStrength = (parseFloat(lorasData[loraIndex].clipStrength) - 0.05).toFixed(2);
|
||||||
|
|
||||||
|
const newValue = formatLoraValue(lorasData);
|
||||||
|
widget.value = newValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clip strength display
|
||||||
|
const clipStrengthEl = document.createElement("input");
|
||||||
|
clipStrengthEl.type = "text";
|
||||||
|
clipStrengthEl.value = typeof clipStrength === 'number' ? clipStrength.toFixed(2) : Number(clipStrength).toFixed(2);
|
||||||
|
Object.assign(clipStrengthEl.style, {
|
||||||
|
minWidth: "50px",
|
||||||
|
width: "50px",
|
||||||
|
textAlign: "center",
|
||||||
|
color: active ? "rgba(200, 215, 240, 0.9)" : "rgba(200, 215, 240, 0.6)",
|
||||||
|
fontSize: "13px",
|
||||||
|
background: "none",
|
||||||
|
border: "1px solid transparent",
|
||||||
|
padding: "2px 4px",
|
||||||
|
borderRadius: "3px",
|
||||||
|
outline: "none",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add hover effect
|
||||||
|
clipStrengthEl.addEventListener('mouseenter', () => {
|
||||||
|
clipStrengthEl.style.border = "1px solid rgba(226, 232, 240, 0.2)";
|
||||||
|
});
|
||||||
|
|
||||||
|
clipStrengthEl.addEventListener('mouseleave', () => {
|
||||||
|
if (document.activeElement !== clipStrengthEl) {
|
||||||
|
clipStrengthEl.style.border = "1px solid transparent";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle focus
|
||||||
|
clipStrengthEl.addEventListener('focus', () => {
|
||||||
|
clipStrengthEl.style.border = "1px solid rgba(72, 118, 255, 0.6)";
|
||||||
|
clipStrengthEl.style.background = "rgba(0, 0, 0, 0.2)";
|
||||||
|
// Auto-select all content
|
||||||
|
clipStrengthEl.select();
|
||||||
|
});
|
||||||
|
|
||||||
|
clipStrengthEl.addEventListener('blur', () => {
|
||||||
|
clipStrengthEl.style.border = "1px solid transparent";
|
||||||
|
clipStrengthEl.style.background = "none";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle input changes
|
||||||
|
clipStrengthEl.addEventListener('change', () => {
|
||||||
|
let newValue = parseFloat(clipStrengthEl.value);
|
||||||
|
|
||||||
|
// Validate input
|
||||||
|
if (isNaN(newValue)) {
|
||||||
|
newValue = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update value
|
||||||
|
const lorasData = parseLoraValue(widget.value);
|
||||||
|
const loraIndex = lorasData.findIndex(l => l.name === name);
|
||||||
|
|
||||||
|
if (loraIndex >= 0) {
|
||||||
|
lorasData[loraIndex].clipStrength = newValue.toFixed(2);
|
||||||
|
|
||||||
|
// Update value and trigger callback
|
||||||
|
const newLorasValue = formatLoraValue(lorasData);
|
||||||
|
widget.value = newLorasValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle key events
|
||||||
|
clipStrengthEl.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
clipStrengthEl.blur();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Right arrow for clip
|
||||||
|
const clipRightArrow = createArrowButton("right", () => {
|
||||||
|
// Increase clip strength
|
||||||
|
const lorasData = parseLoraValue(widget.value);
|
||||||
|
const loraIndex = lorasData.findIndex(l => l.name === name);
|
||||||
|
|
||||||
|
if (loraIndex >= 0) {
|
||||||
|
lorasData[loraIndex].clipStrength = (parseFloat(lorasData[loraIndex].clipStrength) + 0.05).toFixed(2);
|
||||||
|
|
||||||
|
const newValue = formatLoraValue(lorasData);
|
||||||
|
widget.value = newValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
clipStrengthControl.appendChild(clipLeftArrow);
|
||||||
|
clipStrengthControl.appendChild(clipStrengthEl);
|
||||||
|
clipStrengthControl.appendChild(clipRightArrow);
|
||||||
|
|
||||||
|
// Assemble clip entry
|
||||||
|
const clipLeftSection = document.createElement("div");
|
||||||
|
Object.assign(clipLeftSection.style, {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
flex: "1",
|
||||||
|
minWidth: "0", // Allow shrinking
|
||||||
|
});
|
||||||
|
|
||||||
|
clipLeftSection.appendChild(clipNameEl);
|
||||||
|
|
||||||
|
clipEl.appendChild(clipLeftSection);
|
||||||
|
clipEl.appendChild(clipStrengthControl);
|
||||||
|
|
||||||
|
// Hover effects for clip entry
|
||||||
|
clipEl.onmouseenter = () => {
|
||||||
|
clipEl.style.backgroundColor = active ? "rgba(70, 75, 95, 0.7)" : "rgba(55, 60, 70, 0.6)";
|
||||||
|
};
|
||||||
|
|
||||||
|
clipEl.onmouseleave = () => {
|
||||||
|
clipEl.style.backgroundColor = active ? "rgba(65, 70, 90, 0.6)" : "rgba(50, 55, 65, 0.5)";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add drag functionality to clip entry
|
||||||
|
initDrag(clipEl, name, widget, true);
|
||||||
|
|
||||||
|
container.appendChild(clipEl);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate height based on number of loras and fixed sizes
|
// Calculate height based on number of loras and fixed sizes
|
||||||
const calculatedHeight = CONTAINER_PADDING + HEADER_HEIGHT + (Math.min(lorasData.length, 5) * LORA_ENTRY_HEIGHT);
|
const calculatedHeight = CONTAINER_PADDING + HEADER_HEIGHT + (Math.min(totalVisibleEntries, 8) * LORA_ENTRY_HEIGHT);
|
||||||
updateWidgetHeight(calculatedHeight);
|
updateWidgetHeight(calculatedHeight);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -921,7 +1153,37 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
return [...filtered, lora];
|
return [...filtered, lora];
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
widgetValue = uniqueValue;
|
// Preserve clip strengths and expanded state when updating the value
|
||||||
|
const oldLoras = parseLoraValue(widgetValue);
|
||||||
|
|
||||||
|
// Apply existing clip strength values and transfer them to the new value
|
||||||
|
const updatedValue = uniqueValue.map(lora => {
|
||||||
|
const existingLora = oldLoras.find(oldLora => oldLora.name === lora.name);
|
||||||
|
|
||||||
|
// If there's an existing lora with the same name, preserve its clip strength and expanded state
|
||||||
|
if (existingLora) {
|
||||||
|
return {
|
||||||
|
...lora,
|
||||||
|
clipStrength: existingLora.clipStrength || lora.strength,
|
||||||
|
expanded: existingLora.hasOwnProperty('expanded') ?
|
||||||
|
existingLora.expanded :
|
||||||
|
Number(existingLora.clipStrength || lora.strength) !== Number(lora.strength)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// For new loras, default clip strength to model strength and expanded to false
|
||||||
|
// unless clipStrength is already different from strength
|
||||||
|
const clipStrength = lora.clipStrength || lora.strength;
|
||||||
|
return {
|
||||||
|
...lora,
|
||||||
|
clipStrength: clipStrength,
|
||||||
|
expanded: lora.hasOwnProperty('expanded') ?
|
||||||
|
lora.expanded :
|
||||||
|
Number(clipStrength) !== Number(lora.strength)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
widgetValue = updatedValue;
|
||||||
renderLoras(widgetValue, widget);
|
renderLoras(widgetValue, widget);
|
||||||
},
|
},
|
||||||
getMinHeight: function() {
|
getMinHeight: function() {
|
||||||
@@ -948,11 +1210,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
widget.callback = callback;
|
widget.callback = callback;
|
||||||
|
|
||||||
widget.serializeValue = () => {
|
widget.serializeValue = () => {
|
||||||
// Add dummy items to avoid the 2-element serialization issue, a bug in comfyui
|
return widgetValue;
|
||||||
return [...widgetValue,
|
|
||||||
{ name: "__dummy_item1__", strength: 0, active: false, _isDummy: true },
|
|
||||||
{ name: "__dummy_item2__", strength: 0, active: false, _isDummy: true }
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
widget.onRemove = () => {
|
widget.onRemove = () => {
|
||||||
@@ -966,6 +1224,8 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
// Function to directly save the recipe without dialog
|
// Function to directly save the recipe without dialog
|
||||||
async function saveRecipeDirectly(widget) {
|
async function saveRecipeDirectly(widget) {
|
||||||
try {
|
try {
|
||||||
|
const prompt = await app.graphToPrompt();
|
||||||
|
console.log(prompt);
|
||||||
// Show loading toast
|
// Show loading toast
|
||||||
if (app && app.extensionManager && app.extensionManager.toast) {
|
if (app && app.extensionManager && app.extensionManager.toast) {
|
||||||
app.extensionManager.toast.add({
|
app.extensionManager.toast.add({
|
||||||
@@ -1014,4 +1274,14 @@ async function saveRecipeDirectly(widget) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if clip entry should be shown - now based on expanded property or initial diff values
|
||||||
|
const shouldShowClipEntry = (loraData) => {
|
||||||
|
// If expanded property exists, use that
|
||||||
|
if (loraData.hasOwnProperty('expanded')) {
|
||||||
|
return loraData.expanded;
|
||||||
|
}
|
||||||
|
// Otherwise use the legacy logic - if values differ, it should be expanded
|
||||||
|
return Number(loraData.strength) !== Number(loraData.clipStrength);
|
||||||
}
|
}
|
||||||
@@ -220,13 +220,8 @@ export function addTagsWidget(node, name, opts, callback) {
|
|||||||
// Set callback
|
// Set callback
|
||||||
widget.callback = callback;
|
widget.callback = callback;
|
||||||
|
|
||||||
// Add serialization method to avoid ComfyUI serialization issues
|
|
||||||
widget.serializeValue = () => {
|
widget.serializeValue = () => {
|
||||||
// Add dummy items to avoid the 2-element serialization issue
|
return widgetValue
|
||||||
return [...widgetValue,
|
|
||||||
{ text: "__dummy_item__", active: false, _isDummy: true },
|
|
||||||
{ text: "__dummy_item__", active: false, _isDummy: true }
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return { minWidth: 300, minHeight: defaultHeight, widget };
|
return { minWidth: 300, minHeight: defaultHeight, widget };
|
||||||
|
|||||||
Reference in New Issue
Block a user