Serve the bench before the catalog and never await ffmpeg on /thumb.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user