From b194b3cefaddd34ce02cc808d557a55b5d961725 Mon Sep 17 00:00:00 2001 From: Towsty Date: Fri, 18 Sep 2026 12:59:06 -0500 Subject: [PATCH] Always live-scan Studio 2 LoRAs on load and Refresh like Settings. Co-authored-by: Cursor --- components/studio2/Bench.vue | 2 +- server/api/studio-2/loras.get.ts | 32 ++++++++++++++++++-------------- shared/studio2/lora-cache.mjs | 5 ++--- tests/studio2-bench.test.mjs | 24 +++++++++++++++--------- 4 files changed, 36 insertions(+), 27 deletions(-) diff --git a/components/studio2/Bench.vue b/components/studio2/Bench.vue index 5f0dbfa..ed6bd6c 100644 --- a/components/studio2/Bench.vue +++ b/components/studio2/Bench.vue @@ -394,7 +394,7 @@ let timer:ReturnType,clock:ReturnType,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) diff --git a/server/api/studio-2/loras.get.ts b/server/api/studio-2/loras.get.ts index 8c63ea5..aa7019e 100644 --- a/server/api/studio-2/loras.get.ts +++ b/server/api/studio-2/loras.get.ts @@ -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) } +}) diff --git a/shared/studio2/lora-cache.mjs b/shared/studio2/lora-cache.mjs index 7989ae3..436e14b 100644 --- a/shared/studio2/lora-cache.mjs +++ b/shared/studio2/lora-cache.mjs @@ -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); diff --git a/tests/studio2-bench.test.mjs b/tests/studio2-bench.test.mjs index 8734662..07466d4 100644 --- a/tests/studio2-bench.test.mjs +++ b/tests/studio2-bench.test.mjs @@ -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:{}}