Serve the bench before the catalog and never await ffmpeg on /thumb.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-09-29 20:35:28 -05:00
co-authored by Cursor
parent a465dd68ec
commit 4ec8ffa8d6
5 changed files with 141 additions and 67 deletions
+65 -33
View File
@@ -280,11 +280,11 @@
aria-label="Previous"
@click="stepFamily(-1)"
>‹</button>
<div class="viewer-frame" :class="{ 'viewer-frame--veiled': selected && isHidden(selected) }">
<div class="viewer-frame">
<p
v-if="selected && isHidden(selected)"
v-show="selected && isHidden(selected)"
class="placeholder-still viewer-veil-label"
>{{ selected.kind==='track' ? 'Track loaded · preview hidden' : 'Output hidden' }}</p>
>{{ selected?.kind==='track' ? 'Track loaded · preview hidden' : 'Output hidden' }}</p>
<template v-if="selected?.kind==='track'">
<div v-show="!isHidden(selected)" class="w-full max-w-xl px-6 py-4">
<AudioPlayer :key="selected.id" :src="`/api/library/tracks/${selected.id}/audio`" :download-name="`${selected.name || 'track'}.${selected.ext || 'wav'}`" :label="selected.name || 'Track'" />
@@ -303,9 +303,9 @@
</template>
<template v-else-if="selected">
<img
v-show="!isHidden(selected)"
:key="selected.id"
class="viewer-still"
:class="{ 'viewer-still--veiled': isHidden(selected) }"
:src="stillThumbUrl(selected.id)"
alt="Selected still"
decoding="async"
@@ -681,7 +681,19 @@ watch(()=>[form.mode,form.engine],([mode,engine])=>{
syncPhotosBindings()
saveBenchDraft(true)
})
watch(()=>form.folderId,id=>{selected.value=null;form.imageAId='';form.imageBId='';form.photos=[];form.startClipId='';loadRevealed(id);if(import.meta.client && form.engine!=='qwen21')form.identityStillId=localStorage.getItem(`studio2-hero:${id}`)||''},{flush:'sync'})
watch(()=>form.folderId,(id,prev)=>{
selected.value=null
// Wipe Photos only when leaving an existing folder — first restore from '' must keep the draft
if(prev){
form.imageAId=''
form.imageBId=''
form.photos=[]
form.startClipId=''
}
loadRevealed(id)
if(import.meta.client && form.engine!=='qwen21')form.identityStillId=localStorage.getItem(`studio2-hero:${id}`)||''
void loadLibraryOnce().catch(()=>null)
},{flush:'sync'})
watch(()=>form.identityStillId,id=>{if(import.meta.client&&form.folderId&&form.engine!=='qwen21')localStorage.setItem(`studio2-hero:${form.folderId}`,id)})
const stills=computed(()=>library.value.stills.filter((s:any)=>s.folderId===form.folderId))
const clips=computed(()=>library.value.clips.filter((s:any)=>s.folderId===form.folderId))
@@ -695,10 +707,11 @@ const musicJobs=computed(()=>studioQueue.value
.map((j:any)=>({...j,live:j.liveJobId?liveMusic.value[j.liveJobId]:null})))
const gpu=computed(()=>gpuStatus(jobs.value,healthState.value,upscales.value,musicJobs.value))
const gpuChipLabel=computed(()=>{
if(!healthState.value) return 'GPU ?'
const raw=String(healthState.value?.comfy?.gpu || healthState.value?.gpuName || healthState.value?.comfy?.device || '').trim()
const short=raw.match(/(\d{3,4})\b/)?.[1]
if(gpu.value.tone==='ready') return short ? `${short} Ready` : 'Ready'
return gpu.value.label
return gpu.value.label || 'GPU ?'
})
const sourceSlot=computed(()=>{const clip=clips.value.find((a:any)=>a.id===form.startClipId);return form.mode==='extend'?(clip?{...clip,kind:'clip'}:null):inputStill.value})
const pickerAssets=computed(()=>picker.value==='startClipId'?clips.value.map((a:any)=>({...a,kind:'clip'})):stills.value.map((a:any)=>({...a,kind:'still'})))
@@ -1457,7 +1470,31 @@ async function queue(){
async function cancel(job:any){await $fetch('/api/studio-2/cancel',{method:'POST',body:{id:job.id}});await refreshJobs()}
async function loadLibraryOnce(){
library.value=await $fetch('/api/studio-2/library')
// Current folder page only — never the yearly Inbox JSON on /
const qs=new URLSearchParams({limit:'50'})
if(form.folderId) qs.set('folderId',form.folderId)
const page=await $fetch<any>(`/api/library?${qs.toString()}`)
library.value={
folders:page.folders||library.value.folders||[],
stills:page.stills||[],
clips:page.clips||[],
tracks:page.tracks||[],
drafts:page.drafts||library.value.drafts||[],
loraAliases:page.loraAliases||library.value.loraAliases||{},
loraTriggers:page.loraTriggers||library.value.loraTriggers||{},
keepPrompts:page.keepPrompts||library.value.keepPrompts
}
}
async function fetchHealth(){
try{
return await Promise.race([
$fetch<any>('/api/health'),
new Promise<never>((_,rej)=>setTimeout(()=>rej(new Error('health_timeout')),800))
])
}catch{
return null
}
}
watch(()=>[
@@ -1470,37 +1507,32 @@ let timer:ReturnType<typeof setTimeout>,clock:ReturnType<typeof setInterval>,dis
onMounted(()=>{
let storage;try{storage=localStorage}catch{}
loraMemory=createLoraMemory(xaigen,storage);loraMemory.sync(form);form.settings.seed=randomSeed()
// First paint: chrome + empty stage + draft from localStorage — do not await library / object_info
// First paint in ~2s: chrome + draft. Do not await library before the prompt works.
restoreBenchDraft()
if(!form.folderId && library.value.folders?.[0]?.id) form.folderId=library.value.folders[0].id
loadRevealed(form.folderId)
clock=setInterval(()=>{now.value=Date.now()},1000)
// Deferred heavy work after shell is interactive
void (async()=>{
void loadLoras(false)
void loadLoras(false)
// Folder page in background — never blocks Generate / prompt
void loadLibraryOnce().then(()=>{
restoreBenchDraft()
if(!form.folderId)form.folderId=library.value.folders[0]?.id||''
loadRevealed(form.folderId)
void applyIncomingGenerationLoad()
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)
if(asset){selected.value={...asset,kind:route.query.clip?'clip':route.query.track?'track':'still'};if(route.query.use==='1'&&selected.value.kind!=='track')restore(selected.value);if(xaigen)toggleReveal(asset.id)}
if(route.query.task==='music')form.mode='music'
}).catch((e:any)=>{error.value=e.data?.statusMessage||e.message})
// Idle poll: jobs + health only — 800ms health budget, chip GPU ? on miss
const poll=async()=>{
try{
await loadLibraryOnce()
restoreBenchDraft()
if(!form.folderId)form.folderId=library.value.folders[0]?.id||''
restoreBenchDraft()
loadRevealed(form.folderId)
await applyIncomingGenerationLoad()
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)
if(asset){selected.value={...asset,kind:route.query.clip?'clip':route.query.track?'track':'still'};if(route.query.use==='1'&&selected.value.kind!=='track')restore(selected.value);if(xaigen)toggleReveal(asset.id)}
if(route.query.task==='music')form.mode='music'
}catch(e:any){error.value=e.data?.statusMessage||e.message}
// Idle poll: jobs + health only — never /api/studio-2/library
const poll=async()=>{
try{
await refreshJobs()
const health=await $fetch<any>('/api/health')
healthState.value=health
}catch(e:any){healthState.value=null;error.value=e.data?.statusMessage||e.message}
if(!disposed)timer=setTimeout(poll,3000)
}
void poll()
})()
await refreshJobs()
healthState.value=await fetchHealth()
}catch(e:any){healthState.value=null;error.value=e.data?.statusMessage||e.message}
if(!disposed)timer=setTimeout(poll,3000)
}
void poll()
if(import.meta.client){
window.addEventListener('keydown',onStageHotkey)
window.addEventListener('pointerdown',onStagePointerDown,true)
+3 -3
View File
@@ -104,9 +104,9 @@
@click.stop="ctx.stepFamily(1)"
>›</button>
<p
v-if="ctx.selected && ctx.isHidden(ctx.selected)"
v-show="ctx.selected && ctx.isHidden(ctx.selected)"
class="m-hidden-note"
>{{ ctx.selected.kind==='track' ? 'Track hidden' : 'Output hidden' }}</p>
>{{ ctx.selected?.kind==='track' ? 'Track hidden' : 'Output hidden' }}</p>
<template v-if="ctx.selected?.kind==='track'">
<div v-show="!ctx.isHidden(ctx.selected)" class="m-audio">
<AudioPlayer :src="`/api/library/tracks/${ctx.selected.id}/audio`" :download-name="`${ctx.selected.name || 'track'}.${ctx.selected.ext || 'wav'}`" :label="ctx.selected.name || 'Track'" />
@@ -122,9 +122,9 @@
/>
<img
v-else-if="ctx.selected"
v-show="!ctx.isHidden(ctx.selected)"
:key="ctx.selected.id"
class="m-stage-still"
:class="{ 'm-stage-still--veiled': ctx.isHidden(ctx.selected) }"
:src="ctx.stillThumbUrl(ctx.selected.id)"
alt="Selected still"
decoding="async"