diff --git a/assets/css/display.css b/assets/css/display.css index b97b516..64b8fbf 100644 --- a/assets/css/display.css +++ b/assets/css/display.css @@ -839,6 +839,14 @@ cursor: not-allowed; } +.queue-foot-error { + margin: 8px 0 0; + color: #fca5a5; + font-size: var(--type-sm); + line-height: 1.35; + word-break: break-word; +} + .queue-foot .tiny { margin-top: 8px; text-align: center; diff --git a/components/studio2/Bench.vue b/components/studio2/Bench.vue index 069a6a4..63e22ad 100644 --- a/components/studio2/Bench.vue +++ b/components/studio2/Bench.vue @@ -389,6 +389,7 @@ {{ submitting ? 'Queuing…' : 'Generate' }} +

{{ error }}

Job queue @@ -1399,52 +1400,117 @@ async function queue(){ 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') - // Button live in one frame — POST may still be in flight - submitting.value=false - } + submitting.value=true + error.value='' try{ if(form.mode==='music'){ logTiming('POST /api/generate/music start') - submitting.value=false void $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}}) .then(()=>{logTiming('POST music ack');void refreshJobs()}) - .catch((e:any)=>{error.value=e.data?.statusMessage||e.message}) + .catch((e:any)=>{error.value=e.data?.statusMessage||e.message||String(e)}) return } if(photoRoleWarning.value){ - jobs.value=jobs.value.filter(j=>j.id!==localId) error.value=photoRoleWarning.value return } + // Plain snapshot only — never structuredClone(form) (Vue proxies / Files throw DataCloneError) + const size={...resolvedSize.value} + const photos=(form.photos||[]) + .filter((p:any)=>p?.stillId) + .map((p:any)=>({stillId:String(p.stillId),role:String(p.role||'change')})) + const promptAction=String(form.promptSections?.action||'') + const promptSections=video.value + ?{ + action:promptAction, + camera:String(form.promptSections?.camera||''), + soundscape:String(form.promptSections?.soundscape||''), + music:String(form.promptSections?.music||''), + locks:String(form.promptSections?.locks||''), + cameraBeats:Array.isArray(form.promptSections?.cameraBeats)?JSON.parse(JSON.stringify(form.promptSections.cameraBeats)):[], + soundscapeChips:[...(form.promptSections?.soundscapeChips||[])], + soundscapeLevel:form.promptSections?.soundscapeLevel||'normal', + musicChip:form.promptSections?.musicChip||'N/A', + dialogue:form.promptSections?.dialogue?JSON.parse(JSON.stringify(form.promptSections.dialogue)):{mode:'none',line:''} + } + :{action:promptAction} + const settings={ + aspect:String(form.settings?.aspect||'auto'), + fps:Number(form.settings?.fps)||24, + steps:Number(form.settings?.steps)||24, + cfg:Number(form.settings?.cfg)||4, + seed:form.settings?.seed ?? null, + seedMode:String(form.settings?.seedMode||'random'), + duration:Number(form.settings?.duration)||5, + turbo:!!form.settings?.turbo, + width:size.width, + height:size.height, + loraStack:Array.isArray(form.settings?.loraStack) + ?form.settings.loraStack.map((l:any)=>({ + name:String(l?.name||l?.file||''), + strength:Number(l?.strength ?? 1), + clipStrength:l?.clipStrength != null ? Number(l.clipStrength) : undefined + })).filter((l:any)=>l.name) + :[] + } + const optimisticRequest={ + mode:String(form.mode), + engine:String(form.engine), + folderId:String(form.folderId||''), + enhancePrompt:!!form.enhancePrompt, + turbo:!!form.turbo, + lockFace:!!form.lockFace, + lockOutfit:!!form.lockOutfit, + hideInput:!!xaigen, + hideOutput:false, + imageAId:String(form.imageAId||''), + imageBId:String(form.imageBId||''), + identityStillId:String(form.identityStillId||''), + startClipId:String(form.startClipId||''), + photos, + promptSections, + imageStyles:{ + positive:[...(form.imageStyles?.positive||[])], + negative:[...(form.imageStyles?.negative||[])] + }, + settings + } + jobs.value=[{ + id:localId, + state:'waiting', + queuedAt:Date.now(), + request:optimisticRequest, + outputs:[], + purgeResult:'Queued' + },...jobs.value] + focusedJobId.value=localId + logTiming('local-card') + syncPhotosBindings() if(form.settings.seedMode!=='locked')form.settings.seed=randomSeed() const body=applyPhotosToRequest({ - ...form, - mode:form.mode==='edit' && parseBatch(batchText.value).length?'iterate':form.mode, - hideInput:xaigen, + mode:optimisticRequest.mode==='edit' && parseBatch(batchText.value).length?'iterate':optimisticRequest.mode, + engine:optimisticRequest.engine, + folderId:optimisticRequest.folderId, + enhancePrompt:optimisticRequest.enhancePrompt, + turbo:optimisticRequest.turbo, + lockFace:optimisticRequest.lockFace, + lockOutfit:optimisticRequest.lockOutfit, + hideInput:optimisticRequest.hideInput, hideOutput:false, - promptSections:video.value?form.promptSections:{action:form.promptSections.action}, - settings:{...form.settings,...resolvedSize.value}, - batch:video.value || form.mode==='edit'?parseBatch(batchText.value):[] + imageAId:optimisticRequest.imageAId, + imageBId:optimisticRequest.imageBId, + identityStillId:optimisticRequest.identityStillId, + startClipId:optimisticRequest.startClipId, + photos, + promptSections, + imageStyles:optimisticRequest.imageStyles, + settings:{...settings,seed:form.settings.seed}, + batch:video.value || form.mode==='edit'?parseBatch(batchText.value):[], + guides:[], + maskId:'', + endStillId:'' }) logTiming('POST /api/studio-2/jobs start') void $fetch('/api/studio-2/jobs',{method:'POST',body}) @@ -1459,12 +1525,14 @@ async function queue(){ }) .catch((e:any)=>{ jobs.value=jobs.value.filter(j=>j.id!==localId) - error.value=e.data?.statusMessage||e.message + error.value=e.data?.statusMessage||e.message||String(e) }) }catch(e:any){ jobs.value=jobs.value.filter(j=>j.id!==localId) - error.value=e.data?.statusMessage||e.message + error.value=e.data?.statusMessage||e.message||String(e) + }finally{ submitting.value=false + logTiming('done') } } async function cancel(job:any){await $fetch('/api/studio-2/cancel',{method:'POST',body:{id:job.id}});await refreshJobs()} @@ -1505,6 +1573,7 @@ watch(()=>[ ],()=>saveBenchDraft(),{deep:true}) let timer:ReturnType,clock:ReturnType,disposed=false onMounted(()=>{ + submitting.value=false let storage;try{storage=localStorage}catch{} loraMemory=createLoraMemory(xaigen,storage);loraMemory.sync(form);form.settings.seed=randomSeed() // First paint in ~2s: chrome + draft. Do not await library before the prompt works.