From 3e90d2ff459f200bd4731ae86c965522b149ba50 Mon Sep 17 00:00:00 2001 From: Towsty Date: Sat, 12 Sep 2026 10:17:03 -0500 Subject: [PATCH] Show clear GPU status and move upscale progress into the queue --- components/VideoUpscaleControl.vue | 6 +++--- components/studio2/Bench.vue | 18 +++++++++++------- components/studio2/GpuStatus.vue | 9 +++++++++ server/api/studio-2/jobs.get.ts | 2 +- server/api/studio-2/upscales.get.ts | 18 ++++++++++++++++++ shared/studio2/status.mjs | 11 +++++++++++ tests/studio2-status-playback.test.mjs | 23 +++++++++++++++++++++++ 7 files changed, 76 insertions(+), 11 deletions(-) create mode 100644 components/studio2/GpuStatus.vue create mode 100644 server/api/studio-2/upscales.get.ts create mode 100644 shared/studio2/status.mjs create mode 100644 tests/studio2-status-playback.test.mjs diff --git a/components/VideoUpscaleControl.vue b/components/VideoUpscaleControl.vue index 5edb8b1..17cc9b3 100644 --- a/components/VideoUpscaleControl.vue +++ b/components/VideoUpscaleControl.vue @@ -1,8 +1,8 @@ diff --git a/components/studio2/GpuStatus.vue b/components/studio2/GpuStatus.vue new file mode 100644 index 0000000..0ab65bb --- /dev/null +++ b/components/studio2/GpuStatus.vue @@ -0,0 +1,9 @@ + + diff --git a/server/api/studio-2/jobs.get.ts b/server/api/studio-2/jobs.get.ts index 78d99ce..dbe623b 100644 --- a/server/api/studio-2/jobs.get.ts +++ b/server/api/studio-2/jobs.get.ts @@ -6,4 +6,4 @@ export default defineEventHandler(event => { const { owner } = assertLibraryOwne } catch { return false; -} }).map(r => { const q = rows.find(q => q.id === r.queueId); return { ...r, state: q?.status === 'cancelled' ? 'cancelled' : q?.status === 'error' ? 'failed' : r.state, error: r.error || q?.lastError }; }); }); +} }).map(r => { const q = rows.find(q => q.id === r.queueId); return { ...r, state: q?.status === 'complete' ? 'complete' : q?.status === 'cancelled' ? 'cancelled' : q?.status === 'error' ? 'failed' : r.state, error: r.error || q?.lastError }; }); }); diff --git a/server/api/studio-2/upscales.get.ts b/server/api/studio-2/upscales.get.ts new file mode 100644 index 0000000..71ba9f3 --- /dev/null +++ b/server/api/studio-2/upscales.get.ts @@ -0,0 +1,18 @@ +import {upscaleRecords} from '../../utils/videoUpscale'; +import {listStudioJobs} from '../../utils/studioQueue'; +import {getClip} from '../../utils/library'; +export default defineEventHandler(event=>{ + const {owner}=assertLibraryOwner(event),records=upscaleRecords(owner); + const rows=listStudioJobs(owner).filter(j=>j.payload.upscale); + const ids=new Set([...rows.map(r=>r.id),...records.map(r=>r.id)]); + return [...ids].flatMap(id=>{ + const row=rows.find(r=>r.id===id),record=records.find(r=>r.id===id); + try { + const clip=getClip(owner,row?.payload.upscale?.sourceId || record?.sourceId); + assertFolderAccess(event,clip.folderId); + const state=['waiting','held','running'].includes(row?.status || '')?row!.status:record?.status || row?.status; + return [{id,folderId:clip.folderId,name:clip.name || 'Video',status:({waiting:'queued',held:'queued',complete:'done',error:'failed',cancelled:'failed'} as any)[state || ''] || state, + progress:record?.progress || 0,width:record?.width,height:record?.height,elapsedMs:record?.elapsedMs || (record?.createdAt?Date.now()-record.createdAt:0),message:record?.message || row?.waitReason,error:record?.error || row?.lastError,outputClipId:record?.outputClipId}]; + }catch{return [];} + }); +}); diff --git a/shared/studio2/status.mjs b/shared/studio2/status.mjs new file mode 100644 index 0000000..df6029d --- /dev/null +++ b/shared/studio2/status.mjs @@ -0,0 +1,11 @@ +export function gpuStatus(jobs, health, upscales=[]) { + if (!health) return {label:'Status unavailable',tone:'unknown',detail:'Checking GPU connection…'} + const upscale=upscales.find(j=>j.status==='running') + if(upscale)return {label:'Upscaling',tone:'busy',detail:upscale.name || 'Video upscale'} + const job=jobs.find(j=>['waking','submitting','rendering','saving'].includes(j.state)) + if(job)return {label:{waking:'Waking up',submitting:'Submitting',rendering:'Generating',saving:'Saving'}[job.state],tone:'busy',detail:job.request?.promptSections?.action || 'Generation in progress'} + if(health.comfy?.busy || health.imageComfy?.busy)return {label:'Busy',tone:'busy',detail:'GPU is working on another job'} + if(health.comfy?.ok || health.imageComfy?.ok)return {label:'Ready',tone:'ready',detail:'No job running'} + if(health.comfy?.asleep)return {label:'Sleeping',tone:'sleeping',detail:'Wakes when a job starts'} + return {label:'Offline',tone:'unknown',detail:'GPU connection unavailable'} +} diff --git a/tests/studio2-status-playback.test.mjs b/tests/studio2-status-playback.test.mjs new file mode 100644 index 0000000..e25ab09 --- /dev/null +++ b/tests/studio2-status-playback.test.mjs @@ -0,0 +1,23 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import {validateHeaderValue} from 'node:http' +import {gpuStatus} from '../shared/studio2/status.mjs' +import {inlineFilename} from '../shared/content-disposition.mjs' +import {readFileSync} from 'node:fs' +test('waiting records never replace the idle GPU status after completion',()=>{ + const waiting={state:'waiting',request:{promptSections:{action:'Old waiting prompt'}}},running={state:'rendering',request:{promptSections:{action:'Current video'}}},health={comfy:{ok:true}}; + assert.equal(gpuStatus([waiting,running],health).detail,'Current video'); + assert.deepEqual(gpuStatus([waiting,{...running,state:'complete'}],health),{label:'Ready',tone:'ready',detail:'No job running'}); + assert.equal(gpuStatus([waiting],health,[{status:'running',name:'Upscale current clip'}]).label,'Upscaling'); + assert.equal(gpuStatus([waiting],{comfy:{asleep:true}}).label,'Sleeping'); +}); +test('Unicode video titles produce valid response headers and retain UTF-8 filename',()=>{ + for(const title of ['A scene… “hello” — 🎥.mp4','日本語.mp4','bad\r\nheader".mp4']){ + const header=inlineFilename(title);assert.doesNotThrow(()=>validateHeaderValue('Content-Disposition',header));assert.match(header,/filename\*=UTF-8''/);assert.doesNotMatch(header,/[\r\n]/); + } + assert.match(inlineFilename('Scene….mp4'),/%E2%80%A6/); +}); +test('Studio 2 upscale button delegates progress to the queue without stretching actions',()=>{ + const bench=readFileSync(new URL('../components/studio2/Bench.vue',import.meta.url),'utf8'),control=readFileSync(new URL('../components/VideoUpscaleControl.vue',import.meta.url),'utf8'); + assert.match(bench,/:clip-id="selected.id" queue-only/);assert.match(bench,/items-start gap-3/);assert.match(bench,/Job queue/);assert.match(control,/state\?\.job && !queueOnly/); +});