mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 15:15:44 -03:00
feat(lora-cycler): add sequential LoRA cycling through filtered pool
Add Lora Cycler node that cycles through LoRAs sequentially from a filtered pool. Supports configurable sort order, strength settings, and persists cycle progress across workflow save/load. Backend: - New LoraCyclerNode with cycle() method - New /api/lm/loras/cycler-list endpoint - LoraService.get_cycler_list() for filtered/sorted list Frontend: - LoraCyclerWidget with Vue.js component - useLoraCyclerState composable - LoraCyclerSettingsView for UI display
This commit is contained in:
208
vue-widgets/src/composables/useLoraCyclerState.ts
Normal file
208
vue-widgets/src/composables/useLoraCyclerState.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import type { ComponentWidget, CyclerConfig, LoraPoolConfig } from './types'
|
||||
|
||||
export interface CyclerLoraItem {
|
||||
file_name: string
|
||||
model_name: string
|
||||
}
|
||||
|
||||
export function useLoraCyclerState(widget: ComponentWidget) {
|
||||
// State refs
|
||||
const currentIndex = ref(1) // 1-based
|
||||
const totalCount = ref(0)
|
||||
const poolConfigHash = ref('')
|
||||
const modelStrength = ref(1.0)
|
||||
const clipStrength = ref(1.0)
|
||||
const useCustomClipRange = ref(false)
|
||||
const sortBy = ref<'filename' | 'model_name'>('filename')
|
||||
const currentLoraName = ref('')
|
||||
const currentLoraFilename = ref('')
|
||||
const isLoading = ref(false)
|
||||
|
||||
// Build config object from current state
|
||||
const buildConfig = (): CyclerConfig => ({
|
||||
current_index: currentIndex.value,
|
||||
total_count: totalCount.value,
|
||||
pool_config_hash: poolConfigHash.value,
|
||||
model_strength: modelStrength.value,
|
||||
clip_strength: clipStrength.value,
|
||||
use_same_clip_strength: !useCustomClipRange.value,
|
||||
sort_by: sortBy.value,
|
||||
current_lora_name: currentLoraName.value,
|
||||
current_lora_filename: currentLoraFilename.value,
|
||||
})
|
||||
|
||||
// Restore state from config object
|
||||
const restoreFromConfig = (config: CyclerConfig) => {
|
||||
currentIndex.value = config.current_index || 1
|
||||
totalCount.value = config.total_count || 0
|
||||
poolConfigHash.value = config.pool_config_hash || ''
|
||||
modelStrength.value = config.model_strength ?? 1.0
|
||||
clipStrength.value = config.clip_strength ?? 1.0
|
||||
useCustomClipRange.value = !(config.use_same_clip_strength ?? true)
|
||||
sortBy.value = config.sort_by || 'filename'
|
||||
currentLoraName.value = config.current_lora_name || ''
|
||||
currentLoraFilename.value = config.current_lora_filename || ''
|
||||
}
|
||||
|
||||
// Generate hash from pool config for change detection
|
||||
const hashPoolConfig = (poolConfig: LoraPoolConfig | null): string => {
|
||||
if (!poolConfig || !poolConfig.filters) {
|
||||
return ''
|
||||
}
|
||||
try {
|
||||
return btoa(JSON.stringify(poolConfig.filters))
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch cycler list from API
|
||||
const fetchCyclerList = async (
|
||||
poolConfig: LoraPoolConfig | null
|
||||
): Promise<CyclerLoraItem[]> => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
|
||||
const requestBody: Record<string, unknown> = {
|
||||
sort_by: sortBy.value,
|
||||
}
|
||||
|
||||
if (poolConfig?.filters) {
|
||||
requestBody.pool_config = poolConfig.filters
|
||||
}
|
||||
|
||||
const response = await fetch('/api/lm/loras/cycler-list', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || 'Failed to fetch cycler list')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Failed to get cycler list')
|
||||
}
|
||||
|
||||
return data.loras || []
|
||||
} catch (error) {
|
||||
console.error('[LoraCyclerState] Error fetching cycler list:', error)
|
||||
throw error
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh list and update state
|
||||
const refreshList = async (poolConfig: LoraPoolConfig | null) => {
|
||||
try {
|
||||
const newHash = hashPoolConfig(poolConfig)
|
||||
const hashChanged = newHash !== poolConfigHash.value
|
||||
|
||||
// Fetch the list
|
||||
const loraList = await fetchCyclerList(poolConfig)
|
||||
|
||||
// Update total count
|
||||
totalCount.value = loraList.length
|
||||
|
||||
// If pool config changed, reset index to 1
|
||||
if (hashChanged) {
|
||||
currentIndex.value = 1
|
||||
poolConfigHash.value = newHash
|
||||
}
|
||||
|
||||
// Clamp index to valid range
|
||||
if (currentIndex.value > totalCount.value) {
|
||||
currentIndex.value = Math.max(1, totalCount.value)
|
||||
}
|
||||
|
||||
// Update current LoRA info
|
||||
if (loraList.length > 0 && currentIndex.value > 0) {
|
||||
const currentLora = loraList[currentIndex.value - 1]
|
||||
if (currentLora) {
|
||||
currentLoraName.value = sortBy.value === 'filename'
|
||||
? currentLora.file_name
|
||||
: (currentLora.model_name || currentLora.file_name)
|
||||
currentLoraFilename.value = currentLora.file_name
|
||||
}
|
||||
} else {
|
||||
currentLoraName.value = ''
|
||||
currentLoraFilename.value = ''
|
||||
}
|
||||
|
||||
return loraList
|
||||
} catch (error) {
|
||||
console.error('[LoraCyclerState] Error refreshing list:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Set index manually
|
||||
const setIndex = (index: number) => {
|
||||
if (index >= 1 && index <= totalCount.value) {
|
||||
currentIndex.value = index
|
||||
}
|
||||
}
|
||||
|
||||
// Computed property to check if clip strength is disabled
|
||||
const isClipStrengthDisabled = computed(() => !useCustomClipRange.value)
|
||||
|
||||
// Watch model strength changes to sync with clip strength when not using custom range
|
||||
watch(modelStrength, (newValue) => {
|
||||
if (!useCustomClipRange.value) {
|
||||
clipStrength.value = newValue
|
||||
}
|
||||
})
|
||||
|
||||
// Watch all state changes and update widget value
|
||||
watch([
|
||||
currentIndex,
|
||||
totalCount,
|
||||
poolConfigHash,
|
||||
modelStrength,
|
||||
clipStrength,
|
||||
useCustomClipRange,
|
||||
sortBy,
|
||||
currentLoraName,
|
||||
currentLoraFilename,
|
||||
], () => {
|
||||
const config = buildConfig()
|
||||
if (widget.updateConfig) {
|
||||
widget.updateConfig(config)
|
||||
} else {
|
||||
widget.value = config
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
return {
|
||||
// State refs
|
||||
currentIndex,
|
||||
totalCount,
|
||||
poolConfigHash,
|
||||
modelStrength,
|
||||
clipStrength,
|
||||
useCustomClipRange,
|
||||
sortBy,
|
||||
currentLoraName,
|
||||
currentLoraFilename,
|
||||
isLoading,
|
||||
|
||||
// Computed
|
||||
isClipStrengthDisabled,
|
||||
|
||||
// Methods
|
||||
buildConfig,
|
||||
restoreFromConfig,
|
||||
hashPoolConfig,
|
||||
fetchCyclerList,
|
||||
refreshList,
|
||||
setIndex,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user