feat(lora-cycler): implement batch queue synchronization with dual-index mechanism

- Add execution_index and next_index fields to CyclerConfig interface
- Introduce beforeQueued hook in widget to handle index shifting for batch executions
- Use execution_index when provided, fall back to current_index for single executions
- Track execution state with Symbol to differentiate first vs subsequent executions
- Update state management to handle dual-index logic for proper LoRA cycling in batch queues
This commit is contained in:
Will Miao
2026-01-22 21:22:52 +08:00
parent bf0291ec0e
commit 2121054cb9
6 changed files with 126 additions and 6 deletions

View File

@@ -56,6 +56,10 @@ class LoraCyclerNode:
clip_strength = float(cycler_config.get("clip_strength", 1.0)) clip_strength = float(cycler_config.get("clip_strength", 1.0))
sort_by = "filename" sort_by = "filename"
# Dual-index mechanism for batch queue synchronization
execution_index = cycler_config.get("execution_index") # Can be None
# next_index_from_config = cycler_config.get("next_index") # Not used on backend
# Get scanner and service # Get scanner and service
scanner = await ServiceRegistry.get_lora_scanner() scanner = await ServiceRegistry.get_lora_scanner()
lora_service = LoraService(scanner) lora_service = LoraService(scanner)
@@ -81,8 +85,16 @@ class LoraCyclerNode:
}, },
} }
# Determine which index to use for this execution
# If execution_index is provided (batch queue case), use it
# Otherwise use current_index (first execution or non-batch case)
if execution_index is not None:
actual_index = execution_index
else:
actual_index = current_index
# Clamp index to valid range (1-based) # Clamp index to valid range (1-based)
clamped_index = max(1, min(current_index, total_count)) clamped_index = max(1, min(actual_index, total_count))
# Get LoRA at current index (convert to 0-based for list access) # Get LoRA at current index (convert to 0-based for list access)
current_lora = lora_list[clamped_index - 1] current_lora = lora_list[clamped_index - 1]

View File

@@ -34,6 +34,9 @@ const props = defineProps<{
// State management // State management
const state = useLoraCyclerState(props.widget) const state = useLoraCyclerState(props.widget)
// Symbol to track if the widget has been executed at least once
const HAS_EXECUTED = Symbol('HAS_EXECUTED')
// Track last known pool config hash // Track last known pool config hash
const lastPoolConfigHash = ref('') const lastPoolConfigHash = ref('')
@@ -124,6 +127,28 @@ onMounted(async () => {
state.restoreFromConfig(props.widget.value as CyclerConfig) state.restoreFromConfig(props.widget.value as CyclerConfig)
} }
// Add beforeQueued hook to handle index shifting for batch queue synchronization
// This ensures each execution uses a different LoRA in the cycle
;(props.widget as any).beforeQueued = () => {
if ((props.widget as any)[HAS_EXECUTED]) {
// After first execution: shift indices (previous next_index becomes execution_index)
state.generateNextIndex()
} else {
// First execution: just initialize next_index (execution_index stays null)
// This means first execution uses current_index from widget
state.initializeNextIndex()
;(props.widget as any)[HAS_EXECUTED] = true
}
// Update the widget value so the indices are included in the serialized config
const config = state.buildConfig()
if ((props.widget as any).updateConfig) {
;(props.widget as any).updateConfig(config)
} else {
props.widget.value = config
}
}
// Mark component as mounted // Mark component as mounted
isMounted.value = true isMounted.value = true

View File

@@ -93,6 +93,9 @@ export interface CyclerConfig {
sort_by: 'filename' | 'model_name' sort_by: 'filename' | 'model_name'
current_lora_name: string // For display current_lora_name: string // For display
current_lora_filename: string current_lora_filename: string
// Dual-index mechanism for batch queue synchronization
execution_index?: number | null // Index to use for current execution
next_index?: number | null // Index for display after execution
} }
export interface ComponentWidget { export interface ComponentWidget {

View File

@@ -19,6 +19,12 @@ export function useLoraCyclerState(widget: ComponentWidget) {
const currentLoraFilename = ref('') const currentLoraFilename = ref('')
const isLoading = ref(false) const isLoading = ref(false)
// Dual-index mechanism for batch queue synchronization
// execution_index: index for generating execution_stack (= previous next_index)
// next_index: index for UI display (= what will be shown after execution)
const executionIndex = ref<number | null>(null)
const nextIndex = ref<number | null>(null)
// Build config object from current state // Build config object from current state
const buildConfig = (): CyclerConfig => ({ const buildConfig = (): CyclerConfig => ({
current_index: currentIndex.value, current_index: currentIndex.value,
@@ -30,6 +36,8 @@ export function useLoraCyclerState(widget: ComponentWidget) {
sort_by: sortBy.value, sort_by: sortBy.value,
current_lora_name: currentLoraName.value, current_lora_name: currentLoraName.value,
current_lora_filename: currentLoraFilename.value, current_lora_filename: currentLoraFilename.value,
execution_index: executionIndex.value,
next_index: nextIndex.value,
}) })
// Restore state from config object // Restore state from config object
@@ -43,6 +51,33 @@ export function useLoraCyclerState(widget: ComponentWidget) {
sortBy.value = config.sort_by || 'filename' sortBy.value = config.sort_by || 'filename'
currentLoraName.value = config.current_lora_name || '' currentLoraName.value = config.current_lora_name || ''
currentLoraFilename.value = config.current_lora_filename || '' currentLoraFilename.value = config.current_lora_filename || ''
// Note: execution_index and next_index are not restored from config
// as they are transient values used only during batch execution
}
// Shift indices for batch queue synchronization
// Previous next_index becomes current execution_index, and generate a new next_index
const generateNextIndex = () => {
executionIndex.value = nextIndex.value // Previous next becomes current execution
// Calculate the next index (wrap to 1 if at end)
const current = executionIndex.value ?? currentIndex.value
let next = current + 1
if (totalCount.value > 0 && next > totalCount.value) {
next = 1
}
nextIndex.value = next
}
// Initialize next_index for first execution (execution_index stays null)
const initializeNextIndex = () => {
if (nextIndex.value === null) {
// First execution uses current_index, so next is current + 1
let next = currentIndex.value + 1
if (totalCount.value > 0 && next > totalCount.value) {
next = 1
}
nextIndex.value = next
}
} }
// Generate hash from pool config for change detection // Generate hash from pool config for change detection
@@ -193,6 +228,8 @@ export function useLoraCyclerState(widget: ComponentWidget) {
currentLoraName, currentLoraName,
currentLoraFilename, currentLoraFilename,
isLoading, isLoading,
executionIndex,
nextIndex,
// Computed // Computed
isClipStrengthDisabled, isClipStrengthDisabled,
@@ -204,5 +241,7 @@ export function useLoraCyclerState(widget: ComponentWidget) {
fetchCyclerList, fetchCyclerList,
refreshList, refreshList,
setIndex, setIndex,
generateNextIndex,
initializeNextIndex,
} }
} }

View File

@@ -1684,7 +1684,7 @@ to {
opacity: 1; opacity: 1;
} }
.lora-cycler-widget[data-v-0f9d3d70] { .lora-cycler-widget[data-v-95dec8bd] {
padding: 6px; padding: 6px;
background: rgba(40, 44, 52, 0.6); background: rgba(40, 44, 52, 0.6);
border-radius: 6px; border-radius: 6px;
@@ -12833,6 +12833,8 @@ function useLoraCyclerState(widget) {
const currentLoraName = ref(""); const currentLoraName = ref("");
const currentLoraFilename = ref(""); const currentLoraFilename = ref("");
const isLoading = ref(false); const isLoading = ref(false);
const executionIndex = ref(null);
const nextIndex = ref(null);
const buildConfig = () => ({ const buildConfig = () => ({
current_index: currentIndex.value, current_index: currentIndex.value,
total_count: totalCount.value, total_count: totalCount.value,
@@ -12842,7 +12844,9 @@ function useLoraCyclerState(widget) {
use_same_clip_strength: !useCustomClipRange.value, use_same_clip_strength: !useCustomClipRange.value,
sort_by: sortBy.value, sort_by: sortBy.value,
current_lora_name: currentLoraName.value, current_lora_name: currentLoraName.value,
current_lora_filename: currentLoraFilename.value current_lora_filename: currentLoraFilename.value,
execution_index: executionIndex.value,
next_index: nextIndex.value
}); });
const restoreFromConfig = (config) => { const restoreFromConfig = (config) => {
currentIndex.value = config.current_index || 1; currentIndex.value = config.current_index || 1;
@@ -12855,6 +12859,24 @@ function useLoraCyclerState(widget) {
currentLoraName.value = config.current_lora_name || ""; currentLoraName.value = config.current_lora_name || "";
currentLoraFilename.value = config.current_lora_filename || ""; currentLoraFilename.value = config.current_lora_filename || "";
}; };
const generateNextIndex = () => {
executionIndex.value = nextIndex.value;
const current = executionIndex.value ?? currentIndex.value;
let next = current + 1;
if (totalCount.value > 0 && next > totalCount.value) {
next = 1;
}
nextIndex.value = next;
};
const initializeNextIndex = () => {
if (nextIndex.value === null) {
let next = currentIndex.value + 1;
if (totalCount.value > 0 && next > totalCount.value) {
next = 1;
}
nextIndex.value = next;
}
};
const hashPoolConfig = (poolConfig) => { const hashPoolConfig = (poolConfig) => {
if (!poolConfig || !poolConfig.filters) { if (!poolConfig || !poolConfig.filters) {
return ""; return "";
@@ -12967,6 +12989,8 @@ function useLoraCyclerState(widget) {
currentLoraName, currentLoraName,
currentLoraFilename, currentLoraFilename,
isLoading, isLoading,
executionIndex,
nextIndex,
// Computed // Computed
isClipStrengthDisabled, isClipStrengthDisabled,
// Methods // Methods
@@ -12975,7 +12999,9 @@ function useLoraCyclerState(widget) {
hashPoolConfig, hashPoolConfig,
fetchCyclerList, fetchCyclerList,
refreshList, refreshList,
setIndex setIndex,
generateNextIndex,
initializeNextIndex
}; };
} }
const _hoisted_1$1 = { class: "lora-cycler-widget" }; const _hoisted_1$1 = { class: "lora-cycler-widget" };
@@ -12988,6 +13014,7 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
setup(__props) { setup(__props) {
const props = __props; const props = __props;
const state = useLoraCyclerState(props.widget); const state = useLoraCyclerState(props.widget);
const HAS_EXECUTED = Symbol("HAS_EXECUTED");
const lastPoolConfigHash = ref(""); const lastPoolConfigHash = ref("");
const isMounted = ref(false); const isMounted = ref(false);
const getPoolConfig = () => { const getPoolConfig = () => {
@@ -13051,6 +13078,20 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
if (props.widget.value) { if (props.widget.value) {
state.restoreFromConfig(props.widget.value); state.restoreFromConfig(props.widget.value);
} }
props.widget.beforeQueued = () => {
if (props.widget[HAS_EXECUTED]) {
state.generateNextIndex();
} else {
state.initializeNextIndex();
props.widget[HAS_EXECUTED] = true;
}
const config = state.buildConfig();
if (props.widget.updateConfig) {
props.widget.updateConfig(config);
} else {
props.widget.value = config;
}
};
isMounted.value = true; isMounted.value = true;
try { try {
const poolConfig = getPoolConfig(); const poolConfig = getPoolConfig();
@@ -13117,7 +13158,7 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
}; };
} }
}); });
const LoraCyclerWidget = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["__scopeId", "data-v-0f9d3d70"]]); const LoraCyclerWidget = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["__scopeId", "data-v-95dec8bd"]]);
const _hoisted_1 = { class: "json-display-widget" }; const _hoisted_1 = { class: "json-display-widget" };
const _hoisted_2 = { const _hoisted_2 = {
class: "json-content", class: "json-content",

File diff suppressed because one or more lines are too long