Files
aigen/server/api/extend.post.ts
T

106 lines
4.2 KiB
TypeScript

import { existsSync } from 'node:fs'
import { addStudioJob, kickStudioQueue, listStudioJobs, videoJobsBusy } from '~/server/utils/studioQueue'
import { persistLoraFields, parsePostedLoraStack, listStudioLoras } from '~/server/utils/loras'
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(() => ({}))
const clipId = String(body?.clipId || '').trim()
const prompt = String(body?.prompt || '').trim()
if (!clipId) {
throw createError({ statusCode: 400, statusMessage: 'A clip is required to extend' })
}
const { owner } = assertLibraryOwner(event)
const source = getClip(owner, clipId)
assertFolderAccess(event, source.folderId)
const sourcePath = clipVideoPath(owner, source.id)
if (!existsSync(sourcePath)) {
throw createError({ statusCode: 404, statusMessage: 'Source video is missing' })
}
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 = 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 })
}
await listStudioLoras()
const postedStack = parsePostedLoraStack(body?.loraStack ?? body?.lora, 'video')
const loraStack = postedStack.length ? postedStack : parsePostedLoraStack(readLoraStack(source), 'video')
const familyId = source.familyId || crypto.randomUUID()
const turbo = source.turbo !== false
const steps = Number(source.steps || defaultVideoSteps(turbo, workflow))
const seed = Math.floor(Math.random() * 2_147_483_647)
const studio = await addStudioJob({
ownerKey: owner,
familyId,
payload: {
prompt,
promptMid: prompt,
promptPre,
promptPost,
name: nextClipPartName(clipTitle(source)),
folderId: source.folderId,
aspect: source.aspect || '16:9',
width: source.width || 1344,
height: source.height || 768,
steps,
turbo,
seed,
cfg: Number(source.cfg || 0),
fps: Number(source.fps || 24),
samplerName: source.samplerName || 'res_multistep',
scheduler: source.scheduler || 'simple',
duration: durationSeconds,
sound: source.sound !== false,
workflow,
useIdentityRefs: false,
hideThumbnail: source.hideThumbnail,
folderLocked,
referenceStillIds: [null, null, null, null],
extensions: [],
queueAutoRun: false,
extendFromClipId: clipId,
globalLocks: source.globalLocks,
permanenceRefs: source.permanenceRefs,
...persistLoraFields(loraStack)
}
})
await kickStudioQueue()
let latest = listStudioJobs(owner).find(item => item.id === studio.id)
if (!latest?.liveJobId && !(await videoJobsBusy())) {
await kickStudioQueue()
latest = listStudioJobs(owner).find(item => item.id === studio.id)
}
const liveJobId = latest?.liveJobId || ''
return {
jobId: liveJobId || studio.id,
studioJobId: studio.id,
queued: !liveJobId,
clientId: '',
seed,
width: source.width || 1344,
height: source.height || 768,
steps,
duration: durationSeconds,
hideThumbnail: source.hideThumbnail,
folderLocked
}
})