From b91f06405dcd62253c3c31e30262b5c3275e048f Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Mon, 22 Sep 2025 13:42:36 +0800 Subject: [PATCH] feat: support clip strength in LoRA usage tips, fixes #401 --- locales/de.json | 1 + locales/en.json | 1 + locales/es.json | 1 + locales/fr.json | 1 + locales/ja.json | 1 + locales/ko.json | 1 + locales/ru.json | 1 + locales/zh-CN.json | 1 + locales/zh-TW.json | 1 + .../components/ContextMenu/LoraContextMenu.js | 7 ++-- static/js/components/shared/ModelCard.js | 5 +-- static/js/components/shared/ModelModal.js | 1 + static/js/managers/BulkManager.js | 8 ++-- static/js/utils/uiHelpers.js | 42 ++++++++++++++++++- web/comfyui/autocomplete.js | 37 +++++++++++++--- 15 files changed, 90 insertions(+), 19 deletions(-) diff --git a/locales/de.json b/locales/de.json index bcff9fc9..09ce4dec 100644 --- a/locales/de.json +++ b/locales/de.json @@ -727,6 +727,7 @@ "strengthMin": "Stärke Min", "strengthMax": "Stärke Max", "strength": "Stärke", + "clipStrength": "Clip-Stärke", "clipSkip": "Clip Skip", "valuePlaceholder": "Wert", "add": "Hinzufügen" diff --git a/locales/en.json b/locales/en.json index 237755c0..35d59115 100644 --- a/locales/en.json +++ b/locales/en.json @@ -727,6 +727,7 @@ "strengthMin": "Strength Min", "strengthMax": "Strength Max", "strength": "Strength", + "clipStrength": "Clip Strength", "clipSkip": "Clip Skip", "valuePlaceholder": "Value", "add": "Add" diff --git a/locales/es.json b/locales/es.json index a4a8f401..a674b713 100644 --- a/locales/es.json +++ b/locales/es.json @@ -727,6 +727,7 @@ "strengthMin": "Fuerza mínima", "strengthMax": "Fuerza máxima", "strength": "Fuerza", + "clipStrength": "Fuerza de Clip", "clipSkip": "Clip Skip", "valuePlaceholder": "Valor", "add": "Añadir" diff --git a/locales/fr.json b/locales/fr.json index b492d805..519f5ec5 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -727,6 +727,7 @@ "strengthMin": "Force Min", "strengthMax": "Force Max", "strength": "Force", + "clipStrength": "Force Clip", "clipSkip": "Clip Skip", "valuePlaceholder": "Valeur", "add": "Ajouter" diff --git a/locales/ja.json b/locales/ja.json index f2f889d4..5670b167 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -727,6 +727,7 @@ "strengthMin": "強度最小", "strengthMax": "強度最大", "strength": "強度", + "clipStrength": "クリップ強度", "clipSkip": "Clip Skip", "valuePlaceholder": "値", "add": "追加" diff --git a/locales/ko.json b/locales/ko.json index 4f329b89..90bbbb75 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -727,6 +727,7 @@ "strengthMin": "최소 강도", "strengthMax": "최대 강도", "strength": "강도", + "clipStrength": "클립 강도", "clipSkip": "클립 스킵", "valuePlaceholder": "값", "add": "추가" diff --git a/locales/ru.json b/locales/ru.json index a0e78b34..08112670 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -727,6 +727,7 @@ "strengthMin": "Мин. сила", "strengthMax": "Макс. сила", "strength": "Сила", + "clipStrength": "Сила клипа", "clipSkip": "Clip Skip", "valuePlaceholder": "Значение", "add": "Добавить" diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 9b713351..6ef426c9 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -727,6 +727,7 @@ "strengthMin": "最小强度", "strengthMax": "最大强度", "strength": "强度", + "clipStrength": "Clip 强度", "clipSkip": "Clip Skip", "valuePlaceholder": "数值", "add": "添加" diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 1c70b7c3..70e769d1 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -727,6 +727,7 @@ "strengthMin": "最小強度", "strengthMax": "最大強度", "strength": "強度", + "clipStrength": "Clip 強度", "clipSkip": "Clip Skip", "valuePlaceholder": "數值", "add": "新增" diff --git a/static/js/components/ContextMenu/LoraContextMenu.js b/static/js/components/ContextMenu/LoraContextMenu.js index ee7634b6..4b72cafa 100644 --- a/static/js/components/ContextMenu/LoraContextMenu.js +++ b/static/js/components/ContextMenu/LoraContextMenu.js @@ -1,7 +1,7 @@ import { BaseContextMenu } from './BaseContextMenu.js'; import { ModelContextMenuMixin } from './ModelContextMenuMixin.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 { moveManager } from '../../managers/MoveManager.js'; @@ -70,9 +70,8 @@ export class LoraContextMenu extends BaseContextMenu { sendLoraToWorkflow(replaceMode) { const card = this.currentCard; const usageTips = JSON.parse(card.dataset.usage_tips || '{}'); - const strength = usageTips.strength || 1; - const loraSyntax = ``; - + const loraSyntax = buildLoraSyntax(card.dataset.file_name, usageTips); + sendLoraToWorkflow(loraSyntax, replaceMode, 'lora'); } } diff --git a/static/js/components/shared/ModelCard.js b/static/js/components/shared/ModelCard.js index c17b60fd..ba8db595 100644 --- a/static/js/components/shared/ModelCard.js +++ b/static/js/components/shared/ModelCard.js @@ -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 { showModelModal } from './ModelModal.js'; import { toggleShowcase } from './showcase/ShowcaseView.js'; @@ -155,8 +155,7 @@ async function toggleFavorite(card) { function handleSendToWorkflow(card, replaceMode, modelType) { if (modelType === MODEL_TYPES.LORA) { const usageTips = JSON.parse(card.dataset.usage_tips || '{}'); - const strength = usageTips.strength || 1; - const loraSyntax = ``; + const loraSyntax = buildLoraSyntax(card.dataset.file_name, usageTips); sendLoraToWorkflow(loraSyntax, replaceMode, 'lora'); } else { // Checkpoint send functionality - to be implemented diff --git a/static/js/components/shared/ModelModal.js b/static/js/components/shared/ModelModal.js index 081b0a46..b0032f3b 100644 --- a/static/js/components/shared/ModelModal.js +++ b/static/js/components/shared/ModelModal.js @@ -271,6 +271,7 @@ function renderLoraSpecificContent(lora, escapedWords) { + diff --git a/static/js/managers/BulkManager.js b/static/js/managers/BulkManager.js index 1dade6ff..f6fdf7c8 100644 --- a/static/js/managers/BulkManager.js +++ b/static/js/managers/BulkManager.js @@ -1,5 +1,5 @@ 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 { modalManager } from './ModalManager.js'; import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js'; @@ -321,8 +321,7 @@ export class BulkManager { if (metadata) { const usageTips = JSON.parse(metadata.usageTips || '{}'); - const strength = usageTips.strength || 1; - loraSyntaxes.push(``); + loraSyntaxes.push(buildLoraSyntax(metadata.fileName, usageTips)); } else { missingLoras.push(filepath); } @@ -361,8 +360,7 @@ export class BulkManager { if (metadata) { const usageTips = JSON.parse(metadata.usageTips || '{}'); - const strength = usageTips.strength || 1; - loraSyntaxes.push(``); + loraSyntaxes.push(buildLoraSyntax(metadata.fileName, usageTips)); } else { missingLoras.push(filepath); } diff --git a/static/js/utils/uiHelpers.js b/static/js/utils/uiHelpers.js index f4e4091a..3aed6942 100644 --- a/static/js/utils/uiHelpers.js +++ b/static/js/utils/uiHelpers.js @@ -295,10 +295,48 @@ export function getNSFWLevelName(level) { 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 ``; + } + + return ``; +} + export function copyLoraSyntax(card) { const usageTips = JSON.parse(card.dataset.usage_tips || "{}"); - const strength = usageTips.strength || 1; - const baseSyntax = ``; + const baseSyntax = buildLoraSyntax(card.dataset.file_name, usageTips); // Check if trigger words should be included const includeTriggerWords = state.global.settings.includeTriggerWords; diff --git a/web/comfyui/autocomplete.js b/web/comfyui/autocomplete.js index 202026ad..790c062a 100644 --- a/web/comfyui/autocomplete.js +++ b/web/comfyui/autocomplete.js @@ -2,6 +2,19 @@ import { api } from "../../scripts/api.js"; import { app } from "../../scripts/app.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 { constructor(inputElement, modelType = 'loras', options = {}) { this.inputElement = inputElement; @@ -380,8 +393,10 @@ class AutoComplete { // Extract just the filename for LoRA name 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 hasStrength = false; + let clipStrength = null; try { const response = await api.fetchApi(`/lm/loras/usage-tips-by-path?relative_path=${encodeURIComponent(relativePath)}`); if (response.ok) { @@ -389,8 +404,18 @@ class AutoComplete { if (data.success && data.usage_tips) { try { const usageTips = JSON.parse(data.usage_tips); - if (usageTips.strength && typeof usageTips.strength === 'number') { - strength = usageTips.strength; + const parsedStrength = parseUsageTipNumber(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) { console.warn('Failed to parse usage tips JSON:', parseError); @@ -401,8 +426,10 @@ class AutoComplete { console.warn('Failed to fetch usage tips:', error); } - // Format the LoRA code with strength - const loraCode = `, `; + // Format the LoRA code with strength values + const loraCode = clipStrength !== null + ? `, ` + : `, `; const currentValue = this.inputElement.value; const caretPos = this.getCaretPosition();