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