Split video/edit job state so the sidecar can edit concurrently, and hide v2 identity-ref thumbnails when the input still is hidden. Co-authored-by: Cursor <cursoragent@cursor.com>
180 lines
4.4 KiB
TypeScript
180 lines
4.4 KiB
TypeScript
export type JobStatus = 'queued' | 'uploading' | 'running' | 'complete' | 'error' | 'cancelled' | 'deferred'
|
|
|
|
export interface JobEvent {
|
|
type: string
|
|
message?: string
|
|
progress?: number
|
|
step?: number
|
|
maxStep?: number
|
|
node?: string | null
|
|
filename?: string
|
|
subfolder?: string
|
|
mediaType?: string
|
|
clipId?: string
|
|
stillId?: string
|
|
hideThumbnail?: boolean
|
|
error?: string
|
|
elapsedMs?: number
|
|
busy?: boolean
|
|
queueRunning?: number
|
|
queuePending?: number
|
|
folderLocked?: boolean
|
|
jobId?: string
|
|
draftId?: string
|
|
chainStep?: number
|
|
chainTotal?: number
|
|
chainLabel?: string
|
|
overallProgress?: number
|
|
}
|
|
|
|
export interface Job {
|
|
id: string
|
|
kind?: 'video' | 'edit'
|
|
promptId?: string
|
|
clientId: string
|
|
status: JobStatus
|
|
message: string
|
|
progress: number
|
|
step: number
|
|
maxStep: number
|
|
startedAt: number
|
|
video?: { filename: string; subfolder: string; type: string }
|
|
clipId?: string
|
|
stillId?: string
|
|
hideThumbnail?: boolean
|
|
library?: {
|
|
ownerKey: string
|
|
folderId: string
|
|
hideThumbnail: boolean
|
|
hideInput?: boolean
|
|
folderLocked?: boolean
|
|
name?: string
|
|
prompt: string
|
|
aspect: string
|
|
width: number
|
|
height: number
|
|
steps: number
|
|
turbo: boolean
|
|
seed: number
|
|
cfg?: number
|
|
fps?: number
|
|
samplerName?: string
|
|
scheduler?: string
|
|
thumb?: Buffer
|
|
imageName?: string
|
|
imageSubfolder?: string
|
|
stillId?: string
|
|
stillFilename?: string
|
|
duration?: number
|
|
sound?: boolean
|
|
draftId?: string
|
|
extensions?: { prompt: string; duration: number }[]
|
|
chainIndex?: number
|
|
chainStep?: number
|
|
chainTotal?: number
|
|
chainLabel?: string
|
|
extendTmpDir?: string
|
|
extendPart1Path?: string
|
|
extendSourceClipId?: string
|
|
familyId?: string
|
|
parentClipId?: string
|
|
workflow?: 'v1' | 'v2'
|
|
}
|
|
error?: string
|
|
socketReady?: boolean
|
|
saving?: boolean
|
|
savedPromptId?: string
|
|
segmentBuffer?: Buffer
|
|
events: JobEvent[]
|
|
listeners: Set<(event: JobEvent) => void>
|
|
}
|
|
|
|
const jobs = new Map<string, Job>()
|
|
const MAX_JOBS = 40
|
|
|
|
export function createJob(): Job {
|
|
const job: Job = {
|
|
id: crypto.randomUUID(),
|
|
clientId: crypto.randomUUID(),
|
|
status: 'queued',
|
|
message: 'Queued',
|
|
progress: 0,
|
|
step: 0,
|
|
maxStep: 0,
|
|
startedAt: Date.now(),
|
|
events: [],
|
|
listeners: new Set()
|
|
}
|
|
jobs.set(job.id, job)
|
|
while (jobs.size > MAX_JOBS) {
|
|
const oldest = jobs.keys().next().value
|
|
if (oldest) jobs.delete(oldest)
|
|
}
|
|
return job
|
|
}
|
|
|
|
export function getJob(id: string) {
|
|
return jobs.get(id)
|
|
}
|
|
|
|
export function listJobs() {
|
|
return [...jobs.values()]
|
|
}
|
|
|
|
export function jobSnapshot(job: Job) {
|
|
return {
|
|
jobId: job.id,
|
|
kind: job.kind || 'video',
|
|
type: 'snapshot' as const,
|
|
status: job.status,
|
|
message: job.message,
|
|
progress: job.progress,
|
|
step: job.step,
|
|
maxStep: job.maxStep,
|
|
promptId: job.promptId,
|
|
elapsedMs: Date.now() - job.startedAt,
|
|
filename: job.library?.folderLocked ? undefined : job.video?.filename,
|
|
subfolder: job.library?.folderLocked ? undefined : job.video?.subfolder,
|
|
mediaType: job.library?.folderLocked ? undefined : job.video?.type,
|
|
clipId: job.clipId,
|
|
stillId: job.stillId,
|
|
hideThumbnail: job.hideThumbnail,
|
|
error: job.error,
|
|
folderLocked: job.library?.folderLocked,
|
|
draftId: job.library?.draftId,
|
|
chainStep: job.library?.chainStep,
|
|
chainTotal: job.library?.chainTotal,
|
|
chainLabel: job.library?.chainLabel
|
|
}
|
|
}
|
|
|
|
export function emitJob(job: Job, event: JobEvent) {
|
|
event.jobId = job.id
|
|
if (event.message) job.message = event.message
|
|
if (typeof event.progress === 'number') job.progress = event.progress
|
|
if (typeof event.step === 'number') job.step = event.step
|
|
if (typeof event.maxStep === 'number') job.maxStep = event.maxStep
|
|
if (typeof event.chainStep === 'number') {
|
|
if (job.library) job.library.chainStep = event.chainStep
|
|
}
|
|
if (typeof event.chainTotal === 'number') {
|
|
if (job.library) job.library.chainTotal = event.chainTotal
|
|
}
|
|
if (event.chainLabel && job.library) job.library.chainLabel = event.chainLabel
|
|
event.elapsedMs = Date.now() - job.startedAt
|
|
job.events.push(event)
|
|
if (job.events.length > 200) job.events.shift()
|
|
for (const listener of job.listeners) {
|
|
try {
|
|
listener(event)
|
|
} catch {
|
|
// ignore broken SSE listeners
|
|
}
|
|
}
|
|
}
|
|
|
|
export function subscribeJob(job: Job, listener: (event: JobEvent) => void) {
|
|
job.listeners.add(listener)
|
|
return () => job.listeners.delete(listener)
|
|
}
|