Give video extensions the same pre-text, prompt, and post-text fields as the main form.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-09-02 15:01:20 -05:00
co-authored by Cursor
parent c4e18eeb2c
commit e7298d9693
11 changed files with 195 additions and 83 deletions
+8 -5
View File
@@ -70,13 +70,16 @@ export async function beginExtendFromClip(params: {
throw createError({ statusCode: 404, statusMessage: 'Source video is missing' })
}
const prompt = String(params.prompt || '').trim()
if (!prompt) {
const durationSeconds = parseExtendDuration(params.duration)
const promptPre = params.promptPre != null
? String(params.promptPre).trim()
: String(source.promptPre || '').trim()
const promptPost = params.promptPost != null
? String(params.promptPost).trim()
: String(source.promptPost || '').trim()
if (!composePromptParts(promptPre, prompt, promptPost)) {
throw createError({ statusCode: 400, statusMessage: 'An extension prompt is required' })
}
const durationSeconds = parseExtendDuration(params.duration)
const promptPre = String(params.promptPre ?? source.promptPre ?? '').trim()
const promptPost = String(params.promptPost ?? source.promptPost ?? '').trim()
const composedPrompt = composeShotPrompt({
globalLocks: source.globalLocks,
prompt: composePromptParts(promptPre, prompt, promptPost),
+1 -1
View File
@@ -80,7 +80,7 @@ export interface Job {
duration?: number
sound?: boolean
draftId?: string
extensions?: { prompt: string; duration: number; permanenceRefs?: import('~/utils/globalLocks').PermanenceRef[]; loraName?: string; loraStack?: import('~/utils/loras').LoraStackItem[] }[]
extensions?: import('~/server/utils/library').QueuedExtension[]
chainIndex?: number
chainStep?: number
chainTotal?: number
+2
View File
@@ -98,6 +98,8 @@ export interface PublicFolder {
export interface QueuedExtension {
prompt: string
promptPre?: string
promptPost?: string
duration: number
permanenceRefs?: PermanenceRef[]
loraName?: string
+2 -2
View File
@@ -49,8 +49,8 @@ export interface PendingJob {
samplerName?: string
scheduler?: string
hideInput?: boolean
extensions?: { prompt: string; duration: number; permanenceRefs?: import('~/utils/globalLocks').PermanenceRef[]; loraName?: string; loraStack?: import('~/utils/loras').LoraStackItem[] }[]
remainingExtensions?: { prompt: string; duration: number; permanenceRefs?: import('~/utils/globalLocks').PermanenceRef[]; loraName?: string; loraStack?: import('~/utils/loras').LoraStackItem[] }[]
extensions?: import('~/server/utils/library').QueuedExtension[]
remainingExtensions?: import('~/server/utils/library').QueuedExtension[]
currentClipId?: string
queueId?: string
queueAutoRun?: boolean
+17 -5
View File
@@ -3,6 +3,8 @@ import { join } from 'node:path'
import { getJob, listJobs, type Job } from '~/server/utils/jobs'
import { parsePostedLoraStack, persistLoraFields } from '~/server/utils/loras'
import type { LoraStackItem } from '~/utils/loras'
import { persistPromptWrappers } from '~/utils/promptParts'
import type { QueuedExtension } from '~/server/utils/library'
export type ShotQueueStatus = 'idle' | 'running' | 'paused' | 'complete' | 'error'
export type ShotSegmentStatus = 'pending' | 'running' | 'complete' | 'error'
@@ -10,6 +12,8 @@ export type ShotSegmentStatus = 'pending' | 'running' | 'complete' | 'error'
export interface ShotQueueSegment {
index: number
prompt: string
promptPre?: string
promptPost?: string
duration: number
status: ShotSegmentStatus
clipId?: string
@@ -187,7 +191,7 @@ export async function createShotQueue(params: {
loraName?: string
loraStack?: LoraStackItem[]
initial: { prompt: string; duration: number; permanenceRefs?: import('~/utils/globalLocks').PermanenceRef[]; loraName?: string; loraStack?: LoraStackItem[] }
extensions: { prompt: string; duration: number; permanenceRefs?: import('~/utils/globalLocks').PermanenceRef[]; loraName?: string; loraStack?: LoraStackItem[] }[]
extensions: QueuedExtension[]
jobId?: string
}): Promise<ShotQueue> {
const now = Date.now()
@@ -231,7 +235,8 @@ export async function createShotQueue(params: {
duration: item.duration,
status: 'pending' as const,
permanenceRefs: item.permanenceRefs,
...persistLoraFields(item.loraStack || item.loraName)
...persistLoraFields(item.loraStack || item.loraName),
...persistPromptWrappers(item)
}))
]
}
@@ -473,10 +478,16 @@ export function listOwnersWithQueues() {
.filter(owner => existsSync(queuesPath(owner)))
}
export function remainingFromQueue(queue: ShotQueue): { prompt: string; duration: number; permanenceRefs?: import('~/utils/globalLocks').PermanenceRef[]; loraName?: string; loraStack?: LoraStackItem[] }[] {
export function remainingFromQueue(queue: ShotQueue): QueuedExtension[] {
return queue.segments
.filter(segment => segment.index > 0)
.map(segment => ({ prompt: segment.prompt, duration: segment.duration, permanenceRefs: segment.permanenceRefs, ...persistLoraFields(segment.loraStack || segment.loraName) }))
.map(segment => ({
prompt: segment.prompt,
duration: segment.duration,
permanenceRefs: segment.permanenceRefs,
...persistLoraFields(segment.loraStack || segment.loraName),
...persistPromptWrappers(segment)
}))
}
export function lastCompletedIndex(queue: ShotQueue) {
@@ -491,6 +502,7 @@ export function liveSegmentPrompt(queue: ShotQueue, extensionIndex: number) {
prompt: segment?.prompt || '',
duration: segment?.duration || 5,
permanenceRefs: segment?.permanenceRefs,
...persistLoraFields(segment?.loraStack || segment?.loraName)
...persistLoraFields(segment?.loraStack || segment?.loraName),
...persistPromptWrappers(segment)
}
}
+2 -2
View File
@@ -7,7 +7,7 @@ import { fetchLiveQueue } from '~/server/utils/comfy'
import { isLtxWorkflow, isTextToVideo, isXaigenStudio, LTX_DISABLED_MESSAGE, ltxWorkflowEnabled, parseVideoWorkflow, type VideoWorkflowId } from '~/utils/videoModels'
import { persistLoraFields } from '~/server/utils/loras'
import { resolveLoraStack } from '~/utils/loras'
import { allowIdentityRefs } from '~/utils/globalLocks'
import { allowIdentityRefs, type PermanenceRef } from '~/utils/globalLocks'
export type StudioJobStatus = 'waiting' | 'running' | 'held' | 'complete' | 'error' | 'cancelled'
export type StudioJobKind = 'video' | 'edit'
@@ -39,7 +39,7 @@ export interface StudioJobPayload {
hideInput?: boolean
folderLocked?: boolean
referenceStillIds: Array<string | null>
extensions: { prompt: string; duration: number; permanenceRefs?: PermanenceRef[]; loraName?: string; loraStack?: import('~/utils/loras').LoraStackItem[] }[]
extensions: import('~/server/utils/library').QueuedExtension[]
queueAutoRun: boolean
globalLocks?: string
permanenceRefs?: PermanenceRef[]
+9 -4
View File
@@ -22,7 +22,8 @@ import {
updateShotQueue
} from '~/server/utils/shotQueue'
import { composeShotPrompt, allowIdentityRefs, type PermanenceRef } from '~/utils/globalLocks'
import { composePromptParts } from '~/utils/promptParts'
import { composePromptParts, resolvePromptWrappers } from '~/utils/promptParts'
import type { QueuedExtension } from '~/server/utils/library'
import { persistLoraFields, ensureComfyLoraNames } from '~/server/utils/loras'
import { readLoraStack, resolveLoraStack } from '~/utils/loras'
import type { LoraStackItem } from '~/utils/loras'
@@ -43,7 +44,7 @@ type VideoChainParams = {
fps: number
samplerName: string
scheduler: string
extensions: { prompt: string; duration: number; loraName?: string; loraStack?: LoraStackItem[]; permanenceRefs?: PermanenceRef[] }[]
extensions: QueuedExtension[]
workflow: VideoWorkflowId
duration: number
useIdentityRefs: boolean
@@ -323,11 +324,12 @@ export async function continueQueuedExtensions(
prompt: live.prompt || extensions[i].prompt,
duration: live.duration || extensions[i].duration
}
const wrappers = resolvePromptWrappers(live, extensions[i], job.library)
const shotStack = resolveLoraStack(
readLoraStack(liveQueue).length ? readLoraStack(liveQueue) : (params.loraStack || params.loraName),
live.loraStack || live.loraName || extensions[i]?.loraStack || extensions[i]?.loraName
)
if (!ext.prompt.trim()) {
if (!composePromptParts(wrappers.promptPre, ext.prompt, wrappers.promptPost)) {
throw new Error(`Shot ${i + 2} needs a prompt`)
}
@@ -385,7 +387,10 @@ export async function continueQueuedExtensions(
throw new Error('Could not extract the last frame for the next extension: the frame file was empty')
}
job.library.extendPart1Path = part1Path
job.library.prompt = ext.prompt
job.library.prompt = composePromptParts(wrappers.promptPre, ext.prompt, wrappers.promptPost)
job.library.promptMid = ext.prompt
job.library.promptPre = wrappers.promptPre || undefined
job.library.promptPost = wrappers.promptPost || undefined
if (live.permanenceRefs?.length) {
const refs = [...(job.library.shotPermanenceRefs || [])]
refs[i + 1] = live.permanenceRefs