feat: add LoRA syntax utilities and comprehensive test suite, fixes #600

- Implement core LoRA syntax manipulation functions including:
  - applyLoraValuesToText for updating LoRA strengths and clip values
  - normalizeStrengthValue for consistent numeric formatting
  - shouldIncludeClipStrength for clip strength inclusion logic
  - cleanupLoraSyntax for text normalization
  - debounce utility for input handling

- Add comprehensive test suite covering all utility functions
- Include edge cases for clip strength handling, numeric formatting, and syntax cleanup
- Support both basic and expanded LoRA syntax formats with proper value preservation
- Enable debounced input synchronization for better performance

The utilities provide robust handling of LoRA syntax patterns while maintaining compatibility with existing ComfyUI workflows.
This commit is contained in:
Will Miao
2025-10-29 22:13:54 +08:00
parent 4ef750b206
commit 39586f4a20
6 changed files with 410 additions and 96 deletions

View File

@@ -1,7 +1,6 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import {
LORA_PATTERN,
collectActiveLorasFromChain,
updateConnectedTriggerWords,
chainCallback,
@@ -11,6 +10,7 @@ import {
getNodeFromGraph,
} from "./utils.js";
import { addLorasWidget } from "./loras_widget.js";
import { applyLoraValuesToText, debounce } from "./lora_syntax_utils.js";
app.registerExtension({
name: "LoraManager.LoraLoader",
@@ -114,8 +114,36 @@ app.registerExtension({
shape: 7, // 7 is the shape of the optional input
});
// Add flag to prevent callback loops
// Add flags to prevent callback loops
let isUpdating = false;
let isSyncingInput = false;
const inputWidget = this.widgets[0];
inputWidget.options.getMaxHeight = () => 100;
this.inputWidget = inputWidget;
const scheduleInputSync = debounce((lorasValue) => {
if (isSyncingInput) {
return;
}
isSyncingInput = true;
isUpdating = true;
try {
const nextText = applyLoraValuesToText(
inputWidget.value,
lorasValue
);
if (inputWidget.value !== nextText) {
inputWidget.value = nextText;
}
} finally {
isUpdating = false;
isSyncingInput = false;
}
});
// Get the widget object directly from the returned object
this.lorasWidget = addLorasWidget(
@@ -123,48 +151,24 @@ app.registerExtension({
"loras",
{},
(value) => {
// Collect all active loras from this node and its input chain
const allActiveLoraNames = collectActiveLorasFromChain(this);
// Update trigger words for connected toggle nodes with the aggregated lora names
updateConnectedTriggerWords(this, allActiveLoraNames);
// Prevent recursive calls
if (isUpdating) return;
isUpdating = true;
try {
// Remove loras that are not in the value array
const inputWidget = this.widgets[0];
const currentLoras = value.map((l) => l.name);
// Collect all active loras from this node and its input chain
const allActiveLoraNames = collectActiveLorasFromChain(this);
// Use the constant pattern here as well
let newText = inputWidget.value.replace(
LORA_PATTERN,
(match, name, strength, clipStrength) => {
return currentLoras.includes(name) ? match : "";
}
);
// Clean up multiple spaces, extra commas, and trim; remove trailing comma if it's the only content
newText = newText
.replace(/\s+/g, " ")
.replace(/,\s*,+/g, ",")
.trim();
if (newText === ",") newText = "";
inputWidget.value = newText;
// Update trigger words for connected toggle nodes with the aggregated lora names
updateConnectedTriggerWords(this, allActiveLoraNames);
} finally {
isUpdating = false;
}
scheduleInputSync(value);
}
).widget;
// Update input widget callback
const inputWidget = this.widgets[0];
inputWidget.options.getMaxHeight = () => 100;
this.inputWidget = inputWidget;
const originalCallback = (value) => {
if (isUpdating) return;
isUpdating = true;