feat: support clip strength in LoRA usage tips, fixes #401

This commit is contained in:
Will Miao
2025-09-22 13:42:36 +08:00
parent 49e03d658b
commit b91f06405d
15 changed files with 90 additions and 19 deletions

View File

@@ -727,6 +727,7 @@
"strengthMin": "Stärke Min", "strengthMin": "Stärke Min",
"strengthMax": "Stärke Max", "strengthMax": "Stärke Max",
"strength": "Stärke", "strength": "Stärke",
"clipStrength": "Clip-Stärke",
"clipSkip": "Clip Skip", "clipSkip": "Clip Skip",
"valuePlaceholder": "Wert", "valuePlaceholder": "Wert",
"add": "Hinzufügen" "add": "Hinzufügen"

View File

@@ -727,6 +727,7 @@
"strengthMin": "Strength Min", "strengthMin": "Strength Min",
"strengthMax": "Strength Max", "strengthMax": "Strength Max",
"strength": "Strength", "strength": "Strength",
"clipStrength": "Clip Strength",
"clipSkip": "Clip Skip", "clipSkip": "Clip Skip",
"valuePlaceholder": "Value", "valuePlaceholder": "Value",
"add": "Add" "add": "Add"

View File

@@ -727,6 +727,7 @@
"strengthMin": "Fuerza mínima", "strengthMin": "Fuerza mínima",
"strengthMax": "Fuerza máxima", "strengthMax": "Fuerza máxima",
"strength": "Fuerza", "strength": "Fuerza",
"clipStrength": "Fuerza de Clip",
"clipSkip": "Clip Skip", "clipSkip": "Clip Skip",
"valuePlaceholder": "Valor", "valuePlaceholder": "Valor",
"add": "Añadir" "add": "Añadir"

View File

@@ -727,6 +727,7 @@
"strengthMin": "Force Min", "strengthMin": "Force Min",
"strengthMax": "Force Max", "strengthMax": "Force Max",
"strength": "Force", "strength": "Force",
"clipStrength": "Force Clip",
"clipSkip": "Clip Skip", "clipSkip": "Clip Skip",
"valuePlaceholder": "Valeur", "valuePlaceholder": "Valeur",
"add": "Ajouter" "add": "Ajouter"

View File

@@ -727,6 +727,7 @@
"strengthMin": "強度最小", "strengthMin": "強度最小",
"strengthMax": "強度最大", "strengthMax": "強度最大",
"strength": "強度", "strength": "強度",
"clipStrength": "クリップ強度",
"clipSkip": "Clip Skip", "clipSkip": "Clip Skip",
"valuePlaceholder": "値", "valuePlaceholder": "値",
"add": "追加" "add": "追加"

View File

@@ -727,6 +727,7 @@
"strengthMin": "최소 강도", "strengthMin": "최소 강도",
"strengthMax": "최대 강도", "strengthMax": "최대 강도",
"strength": "강도", "strength": "강도",
"clipStrength": "클립 강도",
"clipSkip": "클립 스킵", "clipSkip": "클립 스킵",
"valuePlaceholder": "값", "valuePlaceholder": "값",
"add": "추가" "add": "추가"

View File

@@ -727,6 +727,7 @@
"strengthMin": "Мин. сила", "strengthMin": "Мин. сила",
"strengthMax": "Макс. сила", "strengthMax": "Макс. сила",
"strength": "Сила", "strength": "Сила",
"clipStrength": "Сила клипа",
"clipSkip": "Clip Skip", "clipSkip": "Clip Skip",
"valuePlaceholder": "Значение", "valuePlaceholder": "Значение",
"add": "Добавить" "add": "Добавить"

View File

@@ -727,6 +727,7 @@
"strengthMin": "最小强度", "strengthMin": "最小强度",
"strengthMax": "最大强度", "strengthMax": "最大强度",
"strength": "强度", "strength": "强度",
"clipStrength": "Clip 强度",
"clipSkip": "Clip Skip", "clipSkip": "Clip Skip",
"valuePlaceholder": "数值", "valuePlaceholder": "数值",
"add": "添加" "add": "添加"

View File

@@ -727,6 +727,7 @@
"strengthMin": "最小強度", "strengthMin": "最小強度",
"strengthMax": "最大強度", "strengthMax": "最大強度",
"strength": "強度", "strength": "強度",
"clipStrength": "Clip 強度",
"clipSkip": "Clip Skip", "clipSkip": "Clip Skip",
"valuePlaceholder": "數值", "valuePlaceholder": "數值",
"add": "新增" "add": "新增"

View File

@@ -1,7 +1,7 @@
import { BaseContextMenu } from './BaseContextMenu.js'; import { BaseContextMenu } from './BaseContextMenu.js';
import { ModelContextMenuMixin } from './ModelContextMenuMixin.js'; import { ModelContextMenuMixin } from './ModelContextMenuMixin.js';
import { getModelApiClient, resetAndReload } from '../../api/modelApiFactory.js'; import { getModelApiClient, resetAndReload } from '../../api/modelApiFactory.js';
import { copyLoraSyntax, sendLoraToWorkflow } from '../../utils/uiHelpers.js'; import { copyLoraSyntax, sendLoraToWorkflow, buildLoraSyntax } from '../../utils/uiHelpers.js';
import { showExcludeModal, showDeleteModal } from '../../utils/modalUtils.js'; import { showExcludeModal, showDeleteModal } from '../../utils/modalUtils.js';
import { moveManager } from '../../managers/MoveManager.js'; import { moveManager } from '../../managers/MoveManager.js';
@@ -70,8 +70,7 @@ export class LoraContextMenu extends BaseContextMenu {
sendLoraToWorkflow(replaceMode) { sendLoraToWorkflow(replaceMode) {
const card = this.currentCard; const card = this.currentCard;
const usageTips = JSON.parse(card.dataset.usage_tips || '{}'); const usageTips = JSON.parse(card.dataset.usage_tips || '{}');
const strength = usageTips.strength || 1; const loraSyntax = buildLoraSyntax(card.dataset.file_name, usageTips);
const loraSyntax = `<lora:${card.dataset.file_name}:${strength}>`;
sendLoraToWorkflow(loraSyntax, replaceMode, 'lora'); sendLoraToWorkflow(loraSyntax, replaceMode, 'lora');
} }

View File

@@ -1,4 +1,4 @@
import { showToast, openCivitai, copyToClipboard, copyLoraSyntax, sendLoraToWorkflow, openExampleImagesFolder } from '../../utils/uiHelpers.js'; import { showToast, openCivitai, copyToClipboard, copyLoraSyntax, sendLoraToWorkflow, openExampleImagesFolder, buildLoraSyntax } from '../../utils/uiHelpers.js';
import { state, getCurrentPageState } from '../../state/index.js'; import { state, getCurrentPageState } from '../../state/index.js';
import { showModelModal } from './ModelModal.js'; import { showModelModal } from './ModelModal.js';
import { toggleShowcase } from './showcase/ShowcaseView.js'; import { toggleShowcase } from './showcase/ShowcaseView.js';
@@ -155,8 +155,7 @@ async function toggleFavorite(card) {
function handleSendToWorkflow(card, replaceMode, modelType) { function handleSendToWorkflow(card, replaceMode, modelType) {
if (modelType === MODEL_TYPES.LORA) { if (modelType === MODEL_TYPES.LORA) {
const usageTips = JSON.parse(card.dataset.usage_tips || '{}'); const usageTips = JSON.parse(card.dataset.usage_tips || '{}');
const strength = usageTips.strength || 1; const loraSyntax = buildLoraSyntax(card.dataset.file_name, usageTips);
const loraSyntax = `<lora:${card.dataset.file_name}:${strength}>`;
sendLoraToWorkflow(loraSyntax, replaceMode, 'lora'); sendLoraToWorkflow(loraSyntax, replaceMode, 'lora');
} else { } else {
// Checkpoint send functionality - to be implemented // Checkpoint send functionality - to be implemented

View File

@@ -271,6 +271,7 @@ function renderLoraSpecificContent(lora, escapedWords) {
<option value="strength_min">${translate('modals.model.usageTips.strengthMin', {}, 'Strength Min')}</option> <option value="strength_min">${translate('modals.model.usageTips.strengthMin', {}, 'Strength Min')}</option>
<option value="strength_max">${translate('modals.model.usageTips.strengthMax', {}, 'Strength Max')}</option> <option value="strength_max">${translate('modals.model.usageTips.strengthMax', {}, 'Strength Max')}</option>
<option value="strength">${translate('modals.model.usageTips.strength', {}, 'Strength')}</option> <option value="strength">${translate('modals.model.usageTips.strength', {}, 'Strength')}</option>
<option value="clip_strength">${translate('modals.model.usageTips.clipStrength', {}, 'Clip Strength')}</option>
<option value="clip_skip">${translate('modals.model.usageTips.clipSkip', {}, 'Clip Skip')}</option> <option value="clip_skip">${translate('modals.model.usageTips.clipSkip', {}, 'Clip Skip')}</option>
</select> </select>
<input type="number" id="preset-value" step="0.01" placeholder="${translate('modals.model.usageTips.valuePlaceholder', {}, 'Value')}" style="display:none;"> <input type="number" id="preset-value" step="0.01" placeholder="${translate('modals.model.usageTips.valuePlaceholder', {}, 'Value')}" style="display:none;">

View File

@@ -1,5 +1,5 @@
import { state, getCurrentPageState } from '../state/index.js'; import { state, getCurrentPageState } from '../state/index.js';
import { showToast, copyToClipboard, sendLoraToWorkflow } from '../utils/uiHelpers.js'; import { showToast, copyToClipboard, sendLoraToWorkflow, buildLoraSyntax } from '../utils/uiHelpers.js';
import { updateCardsForBulkMode } from '../components/shared/ModelCard.js'; import { updateCardsForBulkMode } from '../components/shared/ModelCard.js';
import { modalManager } from './ModalManager.js'; import { modalManager } from './ModalManager.js';
import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js'; import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js';
@@ -321,8 +321,7 @@ export class BulkManager {
if (metadata) { if (metadata) {
const usageTips = JSON.parse(metadata.usageTips || '{}'); const usageTips = JSON.parse(metadata.usageTips || '{}');
const strength = usageTips.strength || 1; loraSyntaxes.push(buildLoraSyntax(metadata.fileName, usageTips));
loraSyntaxes.push(`<lora:${metadata.fileName}:${strength}>`);
} else { } else {
missingLoras.push(filepath); missingLoras.push(filepath);
} }
@@ -361,8 +360,7 @@ export class BulkManager {
if (metadata) { if (metadata) {
const usageTips = JSON.parse(metadata.usageTips || '{}'); const usageTips = JSON.parse(metadata.usageTips || '{}');
const strength = usageTips.strength || 1; loraSyntaxes.push(buildLoraSyntax(metadata.fileName, usageTips));
loraSyntaxes.push(`<lora:${metadata.fileName}:${strength}>`);
} else { } else {
missingLoras.push(filepath); missingLoras.push(filepath);
} }

View File

@@ -295,10 +295,48 @@ export function getNSFWLevelName(level) {
return 'Unknown'; return 'Unknown';
} }
function parseUsageTipNumber(value) {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (typeof value === 'string') {
const parsed = parseFloat(value);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return null;
}
export function getLoraStrengthsFromUsageTips(usageTips = {}) {
const parsedStrength = parseUsageTipNumber(usageTips.strength);
const clipStrengthSource = usageTips.clip_strength ?? usageTips.clipStrength;
const parsedClipStrength = parseUsageTipNumber(clipStrengthSource);
return {
strength: parsedStrength !== null ? parsedStrength : 1,
hasStrength: parsedStrength !== null,
clipStrength: parsedClipStrength,
hasClipStrength: parsedClipStrength !== null,
};
}
export function buildLoraSyntax(fileName, usageTips = {}) {
const { strength, hasStrength, clipStrength, hasClipStrength } = getLoraStrengthsFromUsageTips(usageTips);
if (hasClipStrength) {
const modelStrength = hasStrength ? strength : 1;
return `<lora:${fileName}:${modelStrength}:${clipStrength}>`;
}
return `<lora:${fileName}:${strength}>`;
}
export function copyLoraSyntax(card) { export function copyLoraSyntax(card) {
const usageTips = JSON.parse(card.dataset.usage_tips || "{}"); const usageTips = JSON.parse(card.dataset.usage_tips || "{}");
const strength = usageTips.strength || 1; const baseSyntax = buildLoraSyntax(card.dataset.file_name, usageTips);
const baseSyntax = `<lora:${card.dataset.file_name}:${strength}>`;
// Check if trigger words should be included // Check if trigger words should be included
const includeTriggerWords = state.global.settings.includeTriggerWords; const includeTriggerWords = state.global.settings.includeTriggerWords;

View File

@@ -2,6 +2,19 @@ import { api } from "../../scripts/api.js";
import { app } from "../../scripts/app.js"; import { app } from "../../scripts/app.js";
import { TextAreaCaretHelper } from "./textarea_caret_helper.js"; import { TextAreaCaretHelper } from "./textarea_caret_helper.js";
function parseUsageTipNumber(value) {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (typeof value === 'string') {
const parsed = parseFloat(value);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return null;
}
class AutoComplete { class AutoComplete {
constructor(inputElement, modelType = 'loras', options = {}) { constructor(inputElement, modelType = 'loras', options = {}) {
this.inputElement = inputElement; this.inputElement = inputElement;
@@ -380,8 +393,10 @@ class AutoComplete {
// Extract just the filename for LoRA name // Extract just the filename for LoRA name
const fileName = relativePath.split(/[/\\]/).pop().replace(/\.(safetensors|ckpt|pt|bin)$/i, ''); const fileName = relativePath.split(/[/\\]/).pop().replace(/\.(safetensors|ckpt|pt|bin)$/i, '');
// Get usage tips and extract strength // Get usage tips and extract strength information
let strength = 1.0; // Default strength let strength = 1.0; // Default strength
let hasStrength = false;
let clipStrength = null;
try { try {
const response = await api.fetchApi(`/lm/loras/usage-tips-by-path?relative_path=${encodeURIComponent(relativePath)}`); const response = await api.fetchApi(`/lm/loras/usage-tips-by-path?relative_path=${encodeURIComponent(relativePath)}`);
if (response.ok) { if (response.ok) {
@@ -389,8 +404,18 @@ class AutoComplete {
if (data.success && data.usage_tips) { if (data.success && data.usage_tips) {
try { try {
const usageTips = JSON.parse(data.usage_tips); const usageTips = JSON.parse(data.usage_tips);
if (usageTips.strength && typeof usageTips.strength === 'number') { const parsedStrength = parseUsageTipNumber(usageTips.strength);
strength = usageTips.strength; if (parsedStrength !== null) {
strength = parsedStrength;
hasStrength = true;
}
const clipSource = usageTips.clip_strength ?? usageTips.clipStrength;
const parsedClipStrength = parseUsageTipNumber(clipSource);
if (parsedClipStrength !== null) {
clipStrength = parsedClipStrength;
if (!hasStrength) {
strength = 1.0;
}
} }
} catch (parseError) { } catch (parseError) {
console.warn('Failed to parse usage tips JSON:', parseError); console.warn('Failed to parse usage tips JSON:', parseError);
@@ -401,8 +426,10 @@ class AutoComplete {
console.warn('Failed to fetch usage tips:', error); console.warn('Failed to fetch usage tips:', error);
} }
// Format the LoRA code with strength // Format the LoRA code with strength values
const loraCode = `<lora:${fileName}:${strength}>, `; const loraCode = clipStrength !== null
? `<lora:${fileName}:${strength}:${clipStrength}>, `
: `<lora:${fileName}:${strength}>, `;
const currentValue = this.inputElement.value; const currentValue = this.inputElement.value;
const caretPos = this.getCaretPosition(); const caretPos = this.getCaretPosition();