Always live-scan Studio 2 LoRAs on load and Refresh like Settings.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-09-18 12:59:06 -05:00
co-authored by Cursor
parent 7221b6a8cf
commit b194b3cefa
4 changed files with 36 additions and 27 deletions
+1 -1
View File
@@ -394,7 +394,7 @@ let timer:ReturnType<typeof setTimeout>,clock:ReturnType<typeof setInterval>,dis
onMounted(async()=>{
let storage;try{storage=localStorage}catch{}
loraMemory=createLoraMemory(xaigen,storage);loraMemory.sync(form);form.settings.seed=randomSeed()
clock=setInterval(()=>{now.value=Date.now()},1000);void loadLoras()
clock=setInterval(()=>{now.value=Date.now()},1000);void loadLoras(true)
library.value=await $fetch('/api/studio-2/library');form.folderId=library.value.folders[0]?.id||'';loadRevealed(form.folderId)
const id=String(route.query.still||route.query.clip||route.query.track||'')
const asset=[...library.value.stills,...library.value.clips,...(library.value.tracks||[])].find((a:any)=>a.id===id)
+18 -14
View File
@@ -1,15 +1,19 @@
import { readFileSync,writeFileSync,renameSync } from 'node:fs';
import { join } from 'node:path';
import { cachedLoras } from '~/shared/studio2/lora-cache.mjs';
import { listStudioLoras } from '../../utils/loras';
import { studio2Root } from '../../utils/studio2/store';
import { studio2LoraCatalog } from '../../utils/studio2/lora-catalog';
import { readFileSync,writeFileSync,renameSync } from 'node:fs'
import { join } from 'node:path'
import { cachedLoras } from '~/shared/studio2/lora-cache.mjs'
import { listStudioLoras } from '../../utils/loras'
import { studio2Root } from '../../utils/studio2/store'
import { studio2LoraCatalog } from '../../utils/studio2/lora-catalog'
export default defineEventHandler(async event => {
const {owner}=assertLibraryOwner(event),path=join(studio2Root(),`loras-${owner}.cache`);
const listed=await cachedLoras({
read:()=>{try{return JSON.parse(readFileSync(path,'utf8'));}catch{return null;}},
write:value=>{writeFileSync(path+'.tmp',JSON.stringify(value));renameSync(path+'.tmp',path);},
live:()=>listStudioLoras({fresh:true}),fresh:getQuery(event).fresh==='1'
});
return {...listed,...studio2LoraCatalog(owner)};
});
const { owner } = assertLibraryOwner(event)
const path = join(studio2Root(), `loras-${owner}.cache`)
// Same live Comfy scan as Settings (/api/loras?fresh=1). Always fresh so CACHE_MS cannot freeze adds.
const listed = await cachedLoras({
read: () => { try { return JSON.parse(readFileSync(path, 'utf8')) } catch { return null } },
write: value => { writeFileSync(path + '.tmp', JSON.stringify(value)); renameSync(path + '.tmp', path) },
live: () => listStudioLoras({ fresh: true }),
fresh: true
})
return { ...listed, ...studio2LoraCatalog(owner) }
})
+2 -3
View File
@@ -1,11 +1,10 @@
export async function cachedLoras({read,write,live,fresh=false}) {
export async function cachedLoras({read,write,live,fresh: _fresh=false}) {
const cached=read() || {image:[],video:[]};
if (!fresh && (cached.image?.length || cached.video?.length)) return cached;
try {
// Always live-scan. Disk is asleep/empty fallback only — never a stale snapshot that skips Comfy.
const listed=await live();
const image=Array.isArray(listed?.image)?listed.image:[];
const video=Array.isArray(listed?.video)?listed.video:[];
// Comfy asleep / empty: keep disk. Otherwise replace — never merge stale names that freeze adds.
if (!image.length && !video.length) return cached;
const next={image,video};
write(next);
+15 -9
View File
@@ -34,26 +34,32 @@ test('hidden xAIGen canvas and family render no image or video elements',async()
test('cached LoRAs survive sleeping GPU and failed explicit refresh',async()=>{
const cache={image:['Klein/portrait.safetensors'],video:['motion.safetensors']};let queried=0
const opts={read:()=>cache,write:()=>assert.fail('Do not erase cache'),live:async()=>{queried++;throw Error('Comfy asleep')}}
assert.deepEqual(await cachedLoras(opts),cache);assert.equal(queried,0);assert.deepEqual(await cachedLoras({...opts,fresh:true}),cache);assert.equal(queried,1)
assert.deepEqual(await cachedLoras(opts),cache);assert.equal(queried,1)
assert.deepEqual(await cachedLoras({...opts,fresh:true}),cache);assert.equal(queried,2)
})
test('fresh LoRA refresh replaces disk cache so new Comfy names appear',async()=>{
test('bench load and Refresh always live-scan and replace disk so new Comfy names appear',async()=>{
let store={image:['old.safetensors'],video:['old-vid.safetensors']}
const next=await cachedLoras({
read:()=>store,
write:value=>{store=value},
live:async()=>({image:['old.safetensors','new-drop.safetensors'],video:['old-vid.safetensors']}),
fresh:true
live:async()=>({image:['old.safetensors','klein-pokemon_anime_style_klein9b.safetensors'],video:['old-vid.safetensors']}),
fresh:false
})
assert.deepEqual(next.image,['old.safetensors','new-drop.safetensors'])
assert.deepEqual(store.image,['old.safetensors','new-drop.safetensors'])
assert.ok(next.image.includes('klein-pokemon_anime_style_klein9b.safetensors'))
assert.deepEqual(store.image,next.image)
const removed=await cachedLoras({
read:()=>store,
write:value=>{store=value},
live:async()=>({image:['new-drop.safetensors'],video:[]}),
live:async()=>({image:['klein-pokemon_anime_style_klein9b.safetensors'],video:[]}),
fresh:true
})
assert.deepEqual(removed,{image:['new-drop.safetensors'],video:[]})
assert.deepEqual(store,{image:['new-drop.safetensors'],video:[]})
assert.deepEqual(removed,{image:['klein-pokemon_anime_style_klein9b.safetensors'],video:[]})
})
test('bench mounts and Refresh LoRAs request a live scan',()=>{
const src=readFileSync(new URL('../components/studio2/Bench.vue',import.meta.url),'utf8')
assert.match(src,/void loadLoras\(true\)/)
assert.match(src,/loadLoras\(true\)/)
assert.match(src,/\/api\/studio-2\/loras'\+\(fresh\?'\?fresh=1':''\)/)
})
test('source/hero actions are independent and full input restores structured settings',()=>{
const form={identityStillId:'hero',imageAId:'source',promptSections:{action:'old'},settings:{}}