Queue Generate with an immediate local card and stop blocking reload on library or object_info.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-09-29 19:46:55 -05:00
co-authored by Cursor
parent ffdaa7ce42
commit cfabfd37ee
3 changed files with 129 additions and 35 deletions
+94 -21
View File
@@ -1304,16 +1304,63 @@ function selectOutput(item:any){stageMenu.value='';selected.value=item;if(form.m
function restore(item:any){Object.assign(form,restoreAll(form,item,xaigen,loraMemory));form.guides=form.guides||[];batchText.value=''}
function focusJob(job:any){selectOutput(job.outputs?.at(-1)||null);focusedJobId.value=job.id}
async function loadLoras(fresh=false){try{loraNames.value=await $fetch('/api/studio-2/loras'+(fresh?'?fresh=1':''))}catch{error.value='Could not refresh LoRAs. The last loaded list is still available.'}}
async function clearFinished(){await $fetch('/api/studio-2/clear-finished',{method:'POST'});await refresh()}
async function clearFinished(){await $fetch('/api/studio-2/clear-finished',{method:'POST'});await refreshJobs()}
async function refreshJobs(){
const [nextJobs,nextUpscales,queue]=await Promise.all([
$fetch<any[]>('/api/studio-2/jobs'),
$fetch<any[]>('/api/studio-2/upscales'),
$fetch<any>('/api/studio-queue?full=1').catch(()=>({jobs:[]}))
])
jobs.value=nextJobs;upscales.value=nextUpscales
studioQueue.value=Array.isArray(queue)?queue:(queue?.jobs||[])
const musicRows=studioQueue.value.filter((j:any)=>j.kind==='music' && j.liveJobId && !['complete','done'].includes(j.status))
const liveEntries=await Promise.all(musicRows.map(async(j:any)=>{
try{return [j.liveJobId,await $fetch<any>(`/api/generate/${j.liveJobId}`)] as const}
catch{return [j.liveJobId,null] as const}
}))
const nextLive:Record<string,any>={}
for(const [id,snap] of liveEntries){if(snap)nextLive[id]=snap}
liveMusic.value=nextLive
}
async function queue(){
const t0=typeof performance!=='undefined'?performance.now():Date.now()
const logTiming=(label:string)=>{
const t=typeof performance!=='undefined'?performance.now():Date.now()
console.info(`[bench-queue] ${label} +${Math.round(t-t0)}ms`)
}
submitting.value=true;error.value=''
// Optimistic card in <100ms — never await library / object_info before the list updates
const localId=`local-${Date.now()}`
if(form.mode!=='music'){
const optimistic={
id:localId,
state:'waiting',
queuedAt:Date.now(),
request:structuredClone({
...form,
promptSections:video.value?form.promptSections:{action:form.promptSections.action},
settings:{...form.settings,...resolvedSize.value}
}),
outputs:[],
purgeResult:'Queued'
}
jobs.value=[optimistic,...jobs.value]
focusedJobId.value=localId
logTiming('local-card')
}
try{
if(form.mode==='music'){
logTiming('POST /api/generate/music start')
await $fetch('/api/generate/music',{method:'POST',body:{engine:'yue2',tags:music.tags,lyrics:music.lyrics,duration:music.duration,seed:music.seed||'random',folderId:form.folderId,name:music.name}})
await refresh()
logTiming('POST music ack')
void refresh()
return
}
if(photoRoleWarning.value){
jobs.value=jobs.value.filter(j=>j.id!==localId)
error.value=photoRoleWarning.value
return
}
if(photoRoleWarning.value){error.value=photoRoleWarning.value;return}
syncPhotosBindings()
if(form.settings.seedMode!=='locked')form.settings.seed=randomSeed()
const body=applyPhotosToRequest({
@@ -1325,13 +1372,30 @@ async function queue(){
settings:{...form.settings,...resolvedSize.value},
batch:video.value || form.mode==='edit'?parseBatch(batchText.value):[]
})
const queued=await $fetch<any>('/api/studio-2/jobs',{method:'POST',body})
form.settings.seed=queued.seed;form.settings.width=queued.width;form.settings.height=queued.height
await refresh()
}catch(e:any){error.value=e.data?.statusMessage||e.message}
finally{submitting.value=false}
logTiming('POST /api/studio-2/jobs start')
const queued=await Promise.race([
$fetch<any>('/api/studio-2/jobs',{method:'POST',body}),
new Promise<never>((_,rej)=>setTimeout(()=>rej(new Error('queue_ack_timeout')),12000))
]).catch(async(err)=>{
logTiming(`POST fail/timeout: ${err?.message||err}`)
// Keep the local card queued; poll will reconcile
if(String(err?.message||err).includes('queue_ack_timeout')) return null
throw err
})
logTiming('POST ack')
if(queued){
form.settings.seed=queued.seed;form.settings.width=queued.width;form.settings.height=queued.height
jobs.value=jobs.value.map(j=>j.id===localId?{...j,id:queued.id,state:'waiting'}:j)
focusedJobId.value=queued.id
}
// Jobs only — do not refetch the whole library when a job starts
void refreshJobs().then(()=>logTiming('refreshJobs done'))
}catch(e:any){
jobs.value=jobs.value.filter(j=>j.id!==localId)
error.value=e.data?.statusMessage||e.message
}finally{submitting.value=false;logTiming('done')}
}
async function cancel(job:any){await $fetch('/api/studio-2/cancel',{method:'POST',body:{id:job.id}});await refresh()}
async function cancel(job:any){await $fetch('/api/studio-2/cancel',{method:'POST',body:{id:job.id}});await refreshJobs()}
async function refresh(){
const [nextJobs,nextLibrary,nextUpscales,queue]=await Promise.all([
@@ -1369,22 +1433,31 @@ watch(()=>[
music.tags,music.lyrics,music.duration,music.seed,music.name
],()=>saveBenchDraft(),{deep:true})
let timer:ReturnType<typeof setTimeout>,clock:ReturnType<typeof setInterval>,disposed=false
onMounted(async()=>{
onMounted(()=>{
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(true)
library.value=await $fetch('/api/studio-2/library')
restoreBenchDraft()
if(!form.folderId)form.folderId=library.value.folders[0]?.id||''
// Folder watch may have cleared photos when folderId was assigned — re-apply draft photos/prompt.
// First paint: chrome + empty stage + draft from localStorage — do not await library / object_info
restoreBenchDraft()
if(!form.folderId && library.value.folders?.[0]?.id) form.folderId=library.value.folders[0].id
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'
const poll=async()=>{try{await refresh();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()
clock=setInterval(()=>{now.value=Date.now()},1000)
// Deferred heavy work after shell is interactive
void (async()=>{
void loadLoras(false)
try{
library.value=await $fetch('/api/studio-2/library')
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}
const poll=async()=>{try{await refresh();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()
})()
if(import.meta.client){
window.addEventListener('keydown',onStageHotkey)
window.addEventListener('pointerdown',onStagePointerDown,true)