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:
@@ -4,6 +4,7 @@ import { persistLoraFields, parsePostedLoraStack, listStudioLoras } from '~/serv
|
||||
import { readLoraStack } from '~/utils/loras'
|
||||
import { parseExtendDuration } from '~/server/utils/extendChain'
|
||||
import { defaultVideoSteps, isLtxWorkflow, LTX_DISABLED_MESSAGE, ltxWorkflowEnabled, parseVideoWorkflow } from '~/utils/videoModels'
|
||||
import { composePromptParts } from '~/utils/promptParts'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody<{ clipId?: string; prompt?: string; promptPre?: string; promptPost?: string; duration?: number; lora?: unknown; loraStack?: unknown }>(event).catch(() => ({}))
|
||||
@@ -12,9 +13,6 @@ export default defineEventHandler(async (event) => {
|
||||
if (!clipId) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'A clip is required to extend' })
|
||||
}
|
||||
if (!prompt) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'An extension prompt is required' })
|
||||
}
|
||||
|
||||
const { owner } = assertLibraryOwner(event)
|
||||
const source = getClip(owner, clipId)
|
||||
@@ -27,8 +25,15 @@ export default defineEventHandler(async (event) => {
|
||||
const destFolder = publicLibrary(event).folders.find(folder => folder.id === source.folderId)
|
||||
const folderLocked = Boolean(destFolder?.protected && !destFolder.unlocked)
|
||||
const durationSeconds = parseExtendDuration(body?.duration)
|
||||
const promptPre = String(body?.promptPre ?? source.promptPre ?? '').trim()
|
||||
const promptPost = String(body?.promptPost ?? source.promptPost ?? '').trim()
|
||||
const promptPre = body && Object.prototype.hasOwnProperty.call(body, 'promptPre')
|
||||
? String(body.promptPre || '').trim()
|
||||
: String(source.promptPre || '').trim()
|
||||
const promptPost = body && Object.prototype.hasOwnProperty.call(body, 'promptPost')
|
||||
? String(body.promptPost || '').trim()
|
||||
: String(source.promptPost || '').trim()
|
||||
if (!composePromptParts(promptPre, prompt, promptPost)) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'An extension prompt is required' })
|
||||
}
|
||||
const workflow = parseVideoWorkflow(source.workflow)
|
||||
if (isLtxWorkflow(workflow) && !ltxWorkflowEnabled()) {
|
||||
throw createError({ statusCode: 400, statusMessage: LTX_DISABLED_MESSAGE })
|
||||
@@ -47,8 +52,8 @@ export default defineEventHandler(async (event) => {
|
||||
payload: {
|
||||
prompt,
|
||||
promptMid: prompt,
|
||||
promptPre: promptPre || undefined,
|
||||
promptPost: promptPost || undefined,
|
||||
promptPre,
|
||||
promptPost,
|
||||
name: nextClipPartName(clipTitle(source)),
|
||||
folderId: source.folderId,
|
||||
aspect: source.aspect || '16:9',
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { addStudioJob, kickStudioQueue, listStudioJobs, videoJobsBusy } from '~/server/utils/studioQueue'
|
||||
import { listStudioLoras, parsePostedLoraStack, parseShotLoraStacks, persistLoraFields } from '~/server/utils/loras'
|
||||
import type { LoraStackItem } from '~/utils/loras'
|
||||
import { clampVideoCfg } from '~/utils/generationPresets'
|
||||
import { defaultVideoSteps, isLtxWorkflow, isTextToVideo, LTX_DISABLED_MESSAGE, ltxWorkflowEnabled, parseVideoWorkflow, videoEngineOf } from '~/utils/videoModels'
|
||||
import { allowIdentityRefs, normalizePermanenceRefs, resolveGlobalLocks, type PermanenceRef } from '~/utils/globalLocks'
|
||||
import { composePromptParts } from '~/utils/promptParts'
|
||||
import { composePromptParts, persistPromptWrappers } from '~/utils/promptParts'
|
||||
import type { QueuedExtension } from '~/server/utils/library'
|
||||
|
||||
function parseDuration(raw: unknown) {
|
||||
const seconds = Number(raw)
|
||||
@@ -18,21 +18,26 @@ function parseExtendDuration(raw: unknown) {
|
||||
}
|
||||
|
||||
function parseExtensions(raw: string | undefined) {
|
||||
if (!raw) return [] as { prompt: string; duration: number; permanenceRefs?: PermanenceRef[]; loraName?: string; loraStack?: LoraStackItem[] }[]
|
||||
if (!raw) return [] as QueuedExtension[]
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
if (!Array.isArray(parsed)) return []
|
||||
return parsed
|
||||
.map((item: { prompt?: unknown; duration?: unknown; permanenceRefs?: unknown; loraName?: unknown; loraStack?: unknown }) => {
|
||||
.map((item: { prompt?: unknown; promptPre?: unknown; promptPost?: unknown; duration?: unknown; permanenceRefs?: unknown; loraName?: unknown; loraStack?: unknown }) => {
|
||||
const loraStack = parsePostedLoraStack(item?.loraStack ?? item?.loraName, 'video')
|
||||
const wrappers = persistPromptWrappers({
|
||||
...(item && Object.prototype.hasOwnProperty.call(item, 'promptPre') ? { promptPre: String(item.promptPre ?? '') } : {}),
|
||||
...(item && Object.prototype.hasOwnProperty.call(item, 'promptPost') ? { promptPost: String(item.promptPost ?? '') } : {})
|
||||
})
|
||||
return {
|
||||
prompt: String(item?.prompt || '').trim(),
|
||||
duration: parseExtendDuration(item?.duration),
|
||||
permanenceRefs: normalizePermanenceRefs(item?.permanenceRefs),
|
||||
...persistLoraFields(loraStack)
|
||||
...persistLoraFields(loraStack),
|
||||
...wrappers
|
||||
}
|
||||
})
|
||||
.filter(item => item.prompt)
|
||||
.filter(item => composePromptParts(item.promptPre || '', item.prompt, item.promptPost || ''))
|
||||
} catch (error) {
|
||||
if (error && typeof error === 'object' && 'statusCode' in error) throw error
|
||||
return []
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -98,6 +98,8 @@ export interface PublicFolder {
|
||||
|
||||
export interface QueuedExtension {
|
||||
prompt: string
|
||||
promptPre?: string
|
||||
promptPost?: string
|
||||
duration: number
|
||||
permanenceRefs?: PermanenceRef[]
|
||||
loraName?: string
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user