feat(cycler): add preset strength scale (#865)

This commit is contained in:
Will Miao
2026-04-01 11:05:38 +08:00
parent f3b3e0adad
commit 14cb7fec47
14 changed files with 446 additions and 138 deletions

View File

@@ -8,6 +8,8 @@
:model-strength="state.modelStrength.value"
:clip-strength="state.clipStrength.value"
:use-custom-clip-range="state.useCustomClipRange.value"
:use-preset-strength="state.usePresetStrength.value"
:preset-strength-scale="state.presetStrengthScale.value"
:is-clip-strength-disabled="state.isClipStrengthDisabled.value"
:is-loading="state.isLoading.value"
:repeat-count="state.repeatCount.value"
@@ -22,6 +24,8 @@
@update:model-strength="state.modelStrength.value = $event"
@update:clip-strength="state.clipStrength.value = $event"
@update:use-custom-clip-range="handleUseCustomClipRangeChange"
@update:use-preset-strength="state.usePresetStrength.value = $event"
@update:preset-strength-scale="state.presetStrengthScale.value = $event"
@update:repeat-count="handleRepeatCountChange"
@update:include-no-lora="handleIncludeNoLoraChange"
@toggle-pause="handleTogglePause"

View File

@@ -131,6 +131,38 @@
</div>
</div>
<!-- Preset Strength Scale -->
<div class="setting-section">
<div class="section-header-with-toggle">
<label class="setting-label">
Preset Strength Scale
</label>
<button
type="button"
class="toggle-switch"
:class="{ 'toggle-switch--active': usePresetStrength }"
@click="$emit('update:usePresetStrength', !usePresetStrength)"
role="switch"
:aria-checked="usePresetStrength"
title="Use scaled preset strength when enabled"
>
<span class="toggle-switch__track"></span>
<span class="toggle-switch__thumb"></span>
</button>
</div>
<div class="slider-container" :class="{ 'slider-container--disabled': !usePresetStrength }">
<SingleSlider
:min="0"
:max="2"
:value="presetStrengthScale"
:step="0.1"
:default-range="{ min: 0.5, max: 1.0 }"
:disabled="!usePresetStrength"
@update:value="$emit('update:presetStrengthScale', $event)"
/>
</div>
</div>
<!-- Clip Strength -->
<div class="setting-section">
<div class="section-header-with-toggle">
@@ -198,6 +230,8 @@ const props = defineProps<{
modelStrength: number
clipStrength: number
useCustomClipRange: boolean
usePresetStrength: boolean
presetStrengthScale: number
isClipStrengthDisabled: boolean
repeatCount: number
repeatUsed: number
@@ -214,6 +248,8 @@ const emit = defineEmits<{
'update:modelStrength': [value: number]
'update:clipStrength': [value: number]
'update:useCustomClipRange': [value: boolean]
'update:usePresetStrength': [value: boolean]
'update:presetStrengthScale': [value: number]
'update:repeatCount': [value: number]
'update:includeNoLora': [value: boolean]
'toggle-pause': []

View File

@@ -80,6 +80,8 @@ export interface CyclerConfig {
model_strength: number
clip_strength: number
use_same_clip_strength: boolean
use_preset_strength: boolean
preset_strength_scale: number
sort_by: 'filename' | 'model_name'
current_lora_name: string // For display
current_lora_filename: string

View File

@@ -19,6 +19,8 @@ export function useLoraCyclerState(widget: ComponentWidget<CyclerConfig>) {
const modelStrength = ref(1.0)
const clipStrength = ref(1.0)
const useCustomClipRange = ref(false)
const usePresetStrength = ref(false)
const presetStrengthScale = ref(1.0)
const sortBy = ref<'filename' | 'model_name'>('filename')
const currentLoraName = ref('')
const currentLoraFilename = ref('')
@@ -52,6 +54,8 @@ export function useLoraCyclerState(widget: ComponentWidget<CyclerConfig>) {
model_strength: modelStrength.value,
clip_strength: clipStrength.value,
use_same_clip_strength: !useCustomClipRange.value,
use_preset_strength: usePresetStrength.value,
preset_strength_scale: presetStrengthScale.value,
sort_by: sortBy.value,
current_lora_name: currentLoraName.value,
current_lora_filename: currentLoraFilename.value,
@@ -70,6 +74,8 @@ export function useLoraCyclerState(widget: ComponentWidget<CyclerConfig>) {
model_strength: modelStrength.value,
clip_strength: clipStrength.value,
use_same_clip_strength: !useCustomClipRange.value,
use_preset_strength: usePresetStrength.value,
preset_strength_scale: presetStrengthScale.value,
sort_by: sortBy.value,
current_lora_name: currentLoraName.value,
current_lora_filename: currentLoraFilename.value,
@@ -94,6 +100,8 @@ export function useLoraCyclerState(widget: ComponentWidget<CyclerConfig>) {
modelStrength.value = config.model_strength ?? 1.0
clipStrength.value = config.clip_strength ?? 1.0
useCustomClipRange.value = !(config.use_same_clip_strength ?? true)
usePresetStrength.value = config.use_preset_strength ?? false
presetStrengthScale.value = config.preset_strength_scale ?? 1.0
sortBy.value = config.sort_by || 'filename'
currentLoraName.value = config.current_lora_name || ''
currentLoraFilename.value = config.current_lora_filename || ''
@@ -277,6 +285,8 @@ export function useLoraCyclerState(widget: ComponentWidget<CyclerConfig>) {
modelStrength,
clipStrength,
useCustomClipRange,
usePresetStrength,
presetStrengthScale,
sortBy,
currentLoraName,
currentLoraFilename,
@@ -296,6 +306,8 @@ export function useLoraCyclerState(widget: ComponentWidget<CyclerConfig>) {
modelStrength,
clipStrength,
useCustomClipRange,
usePresetStrength,
presetStrengthScale,
sortBy,
currentLoraName,
currentLoraFilename,

View File

@@ -18,7 +18,7 @@ const LORA_RANDOMIZER_WIDGET_MIN_WIDTH = 500
const LORA_RANDOMIZER_WIDGET_MIN_HEIGHT = 448
const LORA_RANDOMIZER_WIDGET_MAX_HEIGHT = LORA_RANDOMIZER_WIDGET_MIN_HEIGHT
const LORA_CYCLER_WIDGET_MIN_WIDTH = 380
const LORA_CYCLER_WIDGET_MIN_HEIGHT = 344
const LORA_CYCLER_WIDGET_MIN_HEIGHT = 408
const LORA_CYCLER_WIDGET_MAX_HEIGHT = LORA_CYCLER_WIDGET_MIN_HEIGHT
const JSON_DISPLAY_WIDGET_MIN_WIDTH = 300
const JSON_DISPLAY_WIDGET_MIN_HEIGHT = 200
@@ -244,7 +244,24 @@ function createLoraCyclerWidget(node) {
forwardMiddleMouseToCanvas(container)
let internalValue: CyclerConfig | undefined
const defaultConfig: CyclerConfig = {
current_index: 1,
total_count: 0,
pool_config_hash: '',
model_strength: 1.0,
clip_strength: 1.0,
use_same_clip_strength: true,
use_preset_strength: false,
preset_strength_scale: 1.0,
sort_by: 'filename',
current_lora_name: '',
current_lora_filename: '',
repeat_count: 1,
repeat_used: 0,
is_paused: false,
include_no_lora: false,
}
let internalValue: CyclerConfig | undefined = defaultConfig
const widget = node.addDOMWidget(
'cycler_config',

View File

@@ -79,6 +79,8 @@ describe('useLoraCyclerState', () => {
model_strength: 1.0,
clip_strength: 1.0,
use_same_clip_strength: true,
use_preset_strength: false,
preset_strength_scale: 1.0,
sort_by: 'filename',
current_lora_name: '',
current_lora_filename: '',
@@ -340,7 +342,8 @@ describe('useLoraCyclerState', () => {
baseModels: ['SD 1.5'],
tags: { include: [], exclude: [] },
folders: { include: [], exclude: [] },
license: { noCreditRequired: false, allowSelling: false }
license: { noCreditRequired: false, allowSelling: false },
namePatterns: { include: [], exclude: [], useRegex: false }
}
})
@@ -349,7 +352,8 @@ describe('useLoraCyclerState', () => {
baseModels: ['SDXL'],
tags: { include: [], exclude: [] },
folders: { include: [], exclude: [] },
license: { noCreditRequired: false, allowSelling: false }
license: { noCreditRequired: false, allowSelling: false },
namePatterns: { include: [], exclude: [], useRegex: false }
}
})
@@ -540,7 +544,8 @@ describe('useLoraCyclerState', () => {
baseModels: ['SDXL'],
tags: { include: [], exclude: [] },
folders: { include: [], exclude: [] },
license: { noCreditRequired: false, allowSelling: false }
license: { noCreditRequired: false, allowSelling: false },
namePatterns: { include: [], exclude: [], useRegex: false }
}
})

View File

@@ -16,6 +16,8 @@ export function createMockCyclerConfig(overrides: Partial<CyclerConfig> = {}): C
model_strength: 1.0,
clip_strength: 1.0,
use_same_clip_strength: true,
use_preset_strength: false,
preset_strength_scale: 1.0,
sort_by: 'filename',
current_lora_name: 'lora1.safetensors',
current_lora_filename: 'lora1.safetensors',
@@ -26,7 +28,7 @@ export function createMockCyclerConfig(overrides: Partial<CyclerConfig> = {}): C
is_paused: false,
include_no_lora: false,
...overrides
}
} as CyclerConfig
}
/**
@@ -42,7 +44,8 @@ export function createMockPoolConfig(overrides: Partial<LoraPoolConfig> = {}): L
license: {
noCreditRequired: false,
allowSelling: false
}
},
namePatterns: { include: [], exclude: [], useRegex: false }
},
preview: { matchCount: 10, lastUpdated: Date.now() },
...overrides
@@ -148,7 +151,8 @@ export const SAMPLE_POOL_CONFIGS = {
baseModels: ['SD 1.5'],
tags: { include: [], exclude: [] },
folders: { include: [], exclude: [] },
license: { noCreditRequired: false, allowSelling: false }
license: { noCreditRequired: false, allowSelling: false },
namePatterns: { include: [], exclude: [], useRegex: false }
}
}),
@@ -158,7 +162,8 @@ export const SAMPLE_POOL_CONFIGS = {
baseModels: ['SDXL'],
tags: { include: [], exclude: [] },
folders: { include: [], exclude: [] },
license: { noCreditRequired: false, allowSelling: false }
license: { noCreditRequired: false, allowSelling: false },
namePatterns: { include: [], exclude: [], useRegex: false }
}
}),
@@ -168,7 +173,8 @@ export const SAMPLE_POOL_CONFIGS = {
baseModels: ['SD 1.5'],
tags: { include: ['anime', 'style'], exclude: ['realistic'] },
folders: { include: [], exclude: [] },
license: { noCreditRequired: false, allowSelling: false }
license: { noCreditRequired: false, allowSelling: false },
namePatterns: { include: [], exclude: [], useRegex: false }
}
}),

View File

@@ -4,17 +4,13 @@
* These tests simulate ComfyUI's execution modes to verify correct LoRA cycling behavior.
*/
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { describe, it, expect, beforeEach } from 'vitest'
import { useLoraCyclerState } from '@/composables/useLoraCyclerState'
import type { CyclerConfig } from '@/composables/types'
import {
createMockWidget,
createMockCyclerConfig,
createMockLoraList,
createMockPoolConfig
} from '../fixtures/mockConfigs'
import { setupFetchMock, resetFetchMock } from '../setup'
import { BatchQueueSimulator, IndexTracker } from '../utils/BatchQueueSimulator'
import { resetFetchMock } from '../setup'
import { BatchQueueSimulator } from '../utils/BatchQueueSimulator'
/**
* Creates a test harness that mimics the LoraCyclerWidget's behavior

View File

@@ -27,7 +27,7 @@ export interface SimulatorOptions {
/**
* Creates execution output based on the current state
*/
function defaultGenerateOutput(executionIndex: number, config: CyclerConfig) {
function defaultGenerateOutput(_executionIndex: number, config: CyclerConfig) {
// Calculate what the next index would be after this execution
let nextIdx = (config.execution_index ?? config.current_index) + 1
if (nextIdx > config.total_count) {