Pause the queue after the current clip for real, and carry global locks plus labeled permanence stills through generate and shot prompts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-28 07:48:09 -05:00
co-authored by Cursor
parent 010ef6add2
commit efef409537
21 changed files with 1196 additions and 115 deletions
+50 -19
View File
@@ -1,6 +1,6 @@
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { getJob, type Job } from '~/server/utils/jobs'
import { getJob, listJobs, type Job } from '~/server/utils/jobs'
export type ShotQueueStatus = 'idle' | 'running' | 'paused' | 'complete' | 'error'
export type ShotSegmentStatus = 'pending' | 'running' | 'complete' | 'error'
@@ -12,6 +12,7 @@ export interface ShotQueueSegment {
status: ShotSegmentStatus
clipId?: string
error?: string
permanenceRefs?: import('~/utils/globalLocks').PermanenceRef[]
}
export interface ShotQueue {
@@ -23,6 +24,7 @@ export interface ShotQueue {
updatedAt: number
status: ShotQueueStatus
autoRun: boolean
stopAfterCurrent?: boolean
currentClipId?: string
currentJobId?: string
stillId?: string
@@ -42,6 +44,8 @@ export interface ShotQueue {
sound?: boolean
useIdentityRefs?: boolean
referenceStillIds?: Array<string | null>
globalLocks?: string
permanenceRefs?: import('~/utils/globalLocks').PermanenceRef[]
segments: ShotQueueSegment[]
lastError?: string
}
@@ -157,8 +161,10 @@ export async function createShotQueue(params: {
sound?: boolean
useIdentityRefs?: boolean
referenceStillIds?: Array<string | null>
initial: { prompt: string; duration: number }
extensions: { prompt: string; duration: number }[]
globalLocks?: string
permanenceRefs?: import('~/utils/globalLocks').PermanenceRef[]
initial: { prompt: string; duration: number; permanenceRefs?: import('~/utils/globalLocks').PermanenceRef[] }
extensions: { prompt: string; duration: number; permanenceRefs?: import('~/utils/globalLocks').PermanenceRef[] }[]
jobId?: string
}): Promise<ShotQueue> {
const now = Date.now()
@@ -171,6 +177,7 @@ export async function createShotQueue(params: {
updatedAt: now,
status: 'running',
autoRun: params.autoRun,
stopAfterCurrent: false,
currentJobId: params.jobId,
stillId: params.stillId,
stillFilename: params.stillFilename,
@@ -189,13 +196,16 @@ export async function createShotQueue(params: {
sound: params.sound,
useIdentityRefs: params.useIdentityRefs,
referenceStillIds: params.referenceStillIds,
globalLocks: params.globalLocks,
permanenceRefs: params.permanenceRefs,
segments: [
{ index: 0, prompt: params.initial.prompt, duration: params.initial.duration, status: 'running' },
{ index: 0, prompt: params.initial.prompt, duration: params.initial.duration, status: 'running', permanenceRefs: params.initial.permanenceRefs },
...params.extensions.map((item, index) => ({
index: index + 1,
prompt: item.prompt,
duration: item.duration,
status: 'pending' as const
status: 'pending' as const,
permanenceRefs: item.permanenceRefs
}))
]
}
@@ -305,6 +315,7 @@ export async function prepareQueueBurst(owner: string, id: string, count: number
delete segment.error
})
queue.autoRun = count === 'all'
queue.stopAfterCurrent = false
queue.status = 'running'
queue.lastError = undefined
queue.updatedAt = Date.now()
@@ -341,23 +352,42 @@ export async function finishQueueBurst(owner: string, id: string, paused: boolea
})
}
export async function pauseShotQueue(owner: string, id: string) {
const jobId = activeJobs.get(id)
if (jobId) {
const job = getJob(jobId)
if (job?.library) job.library.stopAfterCurrent = true
if (job?.library) job.library.queueAutoRun = false
function liveJobForQueue(id: string): Job | undefined {
const mapped = activeJobs.get(id)
if (mapped) {
const job = getJob(mapped)
if (job) return job
}
return listJobs().find(job => (
job.library?.queueId === id
&& (job.status === 'queued' || job.status === 'uploading' || job.status === 'running')
))
}
export async function setShotQueuePause(owner: string, id: string, pause: boolean) {
const live = liveJobForQueue(id)
if (live?.library) {
live.library.stopAfterCurrent = pause
if (pause) live.library.queueAutoRun = false
}
return updateShotQueue(owner, id, (queue) => {
queue.autoRun = false
if (queue.status === 'running') {
queue.lastError = undefined
} else if (queue.status !== 'complete') {
queue.status = 'paused'
if (pause) {
queue.autoRun = false
queue.stopAfterCurrent = true
if (queue.status === 'running') queue.lastError = undefined
else if (queue.status !== 'complete') queue.status = 'paused'
} else {
queue.stopAfterCurrent = false
if (live?.library) live.library.queueAutoRun = queue.autoRun === true
if (queue.status === 'paused') queue.status = 'idle'
}
})
}
export async function pauseShotQueue(owner: string, id: string) {
return setShotQueuePause(owner, id, true)
}
export async function deleteShotQueue(owner: string, id: string) {
if (activeJobs.has(id)) {
throw createError({ statusCode: 409, statusMessage: 'Stop this batch before deleting it' })
@@ -395,10 +425,10 @@ export function listOwnersWithQueues() {
.filter(owner => existsSync(queuesPath(owner)))
}
export function remainingFromQueue(queue: ShotQueue): { prompt: string; duration: number }[] {
export function remainingFromQueue(queue: ShotQueue): { prompt: string; duration: number; permanenceRefs?: import('~/utils/globalLocks').PermanenceRef[] }[] {
return queue.segments
.filter(segment => segment.index > 0)
.map(segment => ({ prompt: segment.prompt, duration: segment.duration }))
.map(segment => ({ prompt: segment.prompt, duration: segment.duration, permanenceRefs: segment.permanenceRefs }))
}
export function lastCompletedIndex(queue: ShotQueue) {
@@ -411,6 +441,7 @@ export function liveSegmentPrompt(queue: ShotQueue, extensionIndex: number) {
const segment = queue.segments.find(item => item.index === extensionIndex + 1)
return {
prompt: segment?.prompt || '',
duration: segment?.duration || 5
duration: segment?.duration || 5,
permanenceRefs: segment?.permanenceRefs
}
}