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
+12 -7
View File
@@ -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',
+11 -6
View File
@@ -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 []