diff --git a/components/SettingsForm.vue b/components/SettingsForm.vue index 6db16a9..643b195 100644 --- a/components/SettingsForm.vue +++ b/components/SettingsForm.vue @@ -760,7 +760,7 @@ async function forceResetGpu() { async function loadLoraKeys() { loraRowsLoading.value = true try { - const listed = await $fetch<{ image?: string[]; video?: string[] }>('/api/loras').catch(() => ({ image: [], video: [] })) + const listed = await $fetch<{ image?: string[]; video?: string[] }>('/api/loras?fresh=1').catch(() => ({ image: [], video: [] })) const library = await $fetch<{ loraAliases?: Record loraTriggers?: Record diff --git a/pages/index.vue b/pages/index.vue index b054115..7f52ad3 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -3915,17 +3915,21 @@ function rememberLoraNames(key: string, names: string[]) { } async function pollLoras() { - const listed = await $fetch<{ image?: string[]; video?: string[] }>('/api/loras').catch(() => ({ image: [], video: [] })) + const listed = await $fetch<{ image?: string[]; video?: string[] }>('/api/loras?fresh=1').catch(() => ({ image: [], video: [] })) const nextVideo = listed.video || [] const nextImage = listed.image || [] - const sameNames = (a: string[], b: string[]) => a.length === b.length && a.every((name, i) => name === b[i]) - if (nextVideo.length && !sameNames(videoLoras.value, nextVideo)) { + const sameNameSet = (a: string[], b: string[]) => { + if (a.length !== b.length) return false + const left = new Set(a) + return b.every(name => left.has(name)) + } + if (nextVideo.length && !sameNameSet(videoLoras.value, nextVideo)) { videoLoras.value = nextVideo rememberLoraNames('aigen-lora-names-video', nextVideo) } else if (!videoLoras.value.length) { videoLoras.value = readStoredLoraNames('aigen-lora-names-video') } - if (nextImage.length && !sameNames(imageLoras.value, nextImage)) { + if (nextImage.length && !sameNameSet(imageLoras.value, nextImage)) { imageLoras.value = nextImage rememberLoraNames('aigen-lora-names-image', nextImage) } else if (!imageLoras.value.length) { diff --git a/server/api/loras.get.ts b/server/api/loras.get.ts index 31c1bf1..27e1992 100644 --- a/server/api/loras.get.ts +++ b/server/api/loras.get.ts @@ -1,7 +1,8 @@ import { listStudioLoras } from '~/server/utils/loras' -export default defineEventHandler(async () => { - const listed = await listStudioLoras() +export default defineEventHandler(async (event) => { + const fresh = String(getQuery(event).fresh || '') === '1' + const listed = await listStudioLoras({ fresh }) return { image: listed.image, video: listed.video diff --git a/server/utils/loras.ts b/server/utils/loras.ts index e82e6a7..d79d2cf 100644 --- a/server/utils/loras.ts +++ b/server/utils/loras.ts @@ -142,10 +142,12 @@ async function discoverFromHost(via: 'video' | 'image') { return { image, video } } -async function loadLoraCache(): Promise { +async function loadLoraCache(options: { fresh?: boolean } = {}): Promise { + const fresh = options.fresh === true const now = Date.now() - if (cache && now - cache.at < CACHE_MS) return cache - if (inflight) return inflight + if (!fresh && cache && now - cache.at < CACHE_MS) return cache + if (inflight && (!fresh || inflightFresh)) return inflight + inflightFresh = fresh inflight = (async () => { const videoHost = comfyConfigured() ? getComfyHost() : '' const imageHost = getBeastImageHost() @@ -161,23 +163,39 @@ async function loadLoraCache(): Promise { image: uniqueNames([...image.image, ...video.image]), video: uniqueNames([...video.video, ...image.video]) } - // Comfy asleep / unreachable returns empty. Keep the last good list so the picker does not vanish. + // Comfy asleep / unreachable returns empty. Keep the last good list so the picker + // does not vanish — but do NOT refresh `at`, or a blip freezes new folder drops out. if (!next.image.length && !next.video.length && cache && (cache.image.length || cache.video.length)) { - cache = { ...cache, at: Date.now() } return cache } + // Prefer the richer list when a partial reply would shrink a known catalog. + if ( + cache + && (next.image.length + next.video.length) < (cache.image.length + cache.video.length) + && next.image.every(name => cache!.image.includes(name) || cache!.video.includes(name)) + && next.video.every(name => cache!.image.includes(name) || cache!.video.includes(name)) + ) { + const merged: LoraCache = { + at: Date.now(), + image: uniqueNames([...cache.image, ...next.image]), + video: uniqueNames([...cache.video, ...next.video]) + } + cache = merged + return merged + } cache = next return next })().finally(() => { inflight = null + inflightFresh = false }) return inflight } -export async function listStudioLoras() { +export async function listStudioLoras(options: { fresh?: boolean } = {}) { const xaigen = isXaigenStudio() try { - const listed = await loadLoraCache() + const listed = await loadLoraCache(options) return { at: listed.at, image: filterLorasForStudio(listed.image, xaigen),