Refresh Studio 2 LoRAs from live Comfy object_info on fresh=1.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-09-18 12:45:48 -05:00
co-authored by Cursor
parent 36d24feb9c
commit 7221b6a8cf
3 changed files with 82 additions and 23 deletions
+57 -21
View File
@@ -44,6 +44,7 @@ type LoraCache = {
let cache: LoraCache | null = null
let inflight: Promise<LoraCache> | null = null
let inflightFresh = false
let loadSeq = 0
function uniqueNames(values: unknown[]) {
const seen = new Set<string>()
@@ -80,14 +81,44 @@ function namesFromObjectInfoNode(info: unknown) {
return uniqueNames(names)
}
async function fetchJson(path: string, via: 'video' | 'image') {
async function fetchJson(path: string, via: 'video' | 'image', timeoutMs = 8000) {
const res = via === 'image'
? await imageComfyFetch(path, { signal: AbortSignal.timeout(8000) })
: await comfyFetch(path, { signal: AbortSignal.timeout(8000) })
? await imageComfyFetch(path, { signal: AbortSignal.timeout(timeoutMs) })
: await comfyFetch(path, { signal: AbortSignal.timeout(timeoutMs) })
if (!res.ok) return null
return res.json().catch(() => null)
}
function collectObjectInfoLoras(all: Record<string, unknown>) {
const image: string[] = []
const video: string[] = []
let any = false
for (const node of OBJECT_INFO_NODES) {
const info = all[node]
if (!info || typeof info !== 'object') continue
const names = namesFromObjectInfoNode(info)
if (!names.length) continue
any = true
if (node === LORA_LOADER) image.push(...names)
else video.push(...names)
if (node !== LORA_LOADER && node !== LORA_MODEL_ONLY) {
image.push(...names)
video.push(...names)
}
}
if (any) return { image: uniqueNames(image), video: uniqueNames(video) }
const imageAll: string[] = []
const videoAll: string[] = []
for (const [classType, info] of Object.entries(all)) {
if (!/lora/i.test(classType)) continue
const names = namesFromObjectInfoNode(info)
if (classType === LORA_LOADER) imageAll.push(...names)
else videoAll.push(...names)
}
return { image: uniqueNames(imageAll), video: uniqueNames(videoAll) }
}
async function fetchModelsLoras(via: 'video' | 'image') {
const payload = await fetchJson('/models/loras', via)
if (Array.isArray(payload)) return uniqueNames(payload)
@@ -97,7 +128,14 @@ async function fetchModelsLoras(via: 'video' | 'image') {
return [] as string[]
}
async function fetchObjectInfoLoras(via: 'video' | 'image') {
async function fetchObjectInfoLoras(via: 'video' | 'image', fresh = false) {
// Full /object_info is Comfy's model-list refresh (UI Refresh) and lists LoraLoader / model-only / rgthree / WanVideoLoraSelect.
if (fresh) {
const all = await fetchJson('/object_info', via, 45_000)
if (all && typeof all === 'object') return collectObjectInfoLoras(all as Record<string, unknown>)
return { image: [] as string[], video: [] as string[] }
}
const image: string[] = []
const video: string[] = []
let any = false
@@ -120,22 +158,14 @@ async function fetchObjectInfoLoras(via: 'video' | 'image') {
return { image: uniqueNames(image), video: uniqueNames(video) }
}
const all = await fetchJson('/object_info', via)
const all = await fetchJson('/object_info', via, 45_000)
if (!all || typeof all !== 'object') return { image: [] as string[], video: [] as string[] }
const imageAll: string[] = []
const videoAll: string[] = []
for (const [classType, info] of Object.entries(all as Record<string, unknown>)) {
if (!/lora/i.test(classType)) continue
const names = namesFromObjectInfoNode(info)
if (classType === LORA_LOADER) imageAll.push(...names)
else videoAll.push(...names)
}
return { image: uniqueNames(imageAll), video: uniqueNames(videoAll) }
return collectObjectInfoLoras(all as Record<string, unknown>)
}
async function discoverFromHost(via: 'video' | 'image') {
async function discoverFromHost(via: 'video' | 'image', fresh = false) {
const fromInfo = await fetchObjectInfoLoras(via, fresh).catch(() => ({ image: [] as string[], video: [] as string[] }))
const models = await fetchModelsLoras(via).catch(() => [] as string[])
const fromInfo = await fetchObjectInfoLoras(via).catch(() => ({ image: [] as string[], video: [] as string[] }))
const image = uniqueNames([...fromInfo.image, ...models])
const video = uniqueNames([...fromInfo.video, ...models])
if (!image.length && video.length) return { image: video, video }
@@ -147,17 +177,19 @@ async function loadLoraCache(options: { fresh?: boolean } = {}): Promise<LoraCac
const fresh = options.fresh === true
const now = Date.now()
if (!fresh && cache && now - cache.at < CACHE_MS) return cache
// Fresh must not reuse a non-fresh inflight (stale list / frozen CACHE_MS window).
if (inflight && (!fresh || inflightFresh)) return inflight
const seq = ++loadSeq
inflightFresh = fresh
inflight = (async () => {
const videoHost = comfyConfigured() ? getComfyHost() : ''
const imageHost = getBeastImageHost()
const same = Boolean(videoHost && imageHost && sameImageHost(videoHost, imageHost))
const video = videoHost
? await discoverFromHost('video').catch(() => ({ image: [] as string[], video: [] as string[] }))
? await discoverFromHost('video', fresh).catch(() => ({ image: [] as string[], video: [] as string[] }))
: { image: [] as string[], video: [] as string[] }
const image = imageHost && !same
? await discoverFromHost('image').catch(() => ({ image: [] as string[], video: [] as string[] }))
? await discoverFromHost('image', fresh).catch(() => ({ image: [] as string[], video: [] as string[] }))
: video
const next: LoraCache = {
at: Date.now(),
@@ -166,15 +198,19 @@ async function loadLoraCache(options: { fresh?: boolean } = {}): Promise<LoraCac
}
// 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.
// Never merge a shorter fresh list back into the old one: deletes must disappear.
// Never merge a shorter live list into the old one: deletes must disappear; adds must appear.
if (!next.image.length && !next.video.length && cache && (cache.image.length || cache.video.length)) {
return cache
}
// Superseded non-fresh must not overwrite a newer fresh result.
if (seq !== loadSeq) return cache || next
cache = next
return next
})().finally(() => {
inflight = null
inflightFresh = false
if (seq === loadSeq) {
inflight = null
inflightFresh = false
}
})
return inflight
}