Add MiniMax and PinkCherry LTX as pickable video engines, with still or text start.
Shot 1 can run text-to-video; later shots still extend from the last frame. Recommend output is also normalized into shot-script cards. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+30
-22
@@ -1,5 +1,6 @@
|
||||
import { frameLength, runGeneration } from '~/server/utils/videoChain'
|
||||
import { createShotQueue, finishQueueBurst, setQueueJob } from '~/server/utils/shotQueue'
|
||||
import { defaultVideoSteps, isLtxWorkflow, isTextToVideo, parseVideoWorkflow } from '~/utils/videoModels'
|
||||
|
||||
function isComfyBusyTimeout(error: unknown) {
|
||||
const err = error as { statusCode?: number; status?: number; data?: { code?: string }; message?: string; statusMessage?: string }
|
||||
@@ -42,16 +43,16 @@ function parseFps(raw: string | undefined) {
|
||||
return fps === 12 || fps === 30 || fps === 24 ? fps : 24
|
||||
}
|
||||
|
||||
function parseCfg(raw: string | undefined, turbo: boolean) {
|
||||
const fallback = turbo ? 1.5 : 4
|
||||
function parseCfg(raw: string | undefined, turbo: boolean, ltx = false) {
|
||||
const fallback = ltx ? 3.5 : (turbo ? 1.5 : 4)
|
||||
const value = Number(raw)
|
||||
if (!Number.isFinite(value)) return fallback
|
||||
const snapped = Math.round(value * 2) / 2
|
||||
return Math.min(10, Math.max(1, snapped))
|
||||
}
|
||||
|
||||
function parseSampler(raw: string | undefined) {
|
||||
return SAMPLERS.has(raw || '') ? raw! : 'res_multistep'
|
||||
function parseSampler(raw: string | undefined, ltx = false) {
|
||||
return SAMPLERS.has(raw || '') ? raw! : (ltx ? 'euler' : 'res_multistep')
|
||||
}
|
||||
|
||||
function parseScheduler(raw: string | undefined) {
|
||||
@@ -86,7 +87,9 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!image) {
|
||||
const workflow = parseVideoWorkflow(fields.workflow)
|
||||
const textToVideo = isTextToVideo(workflow)
|
||||
if (!image && !textToVideo) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'An initial image is required' })
|
||||
}
|
||||
const prompt = (fields.prompt || '').trim()
|
||||
@@ -94,10 +97,13 @@ export default defineEventHandler(async (event) => {
|
||||
throw createError({ statusCode: 400, statusMessage: 'A motion prompt is required' })
|
||||
}
|
||||
const extensions = parseExtensions(fields.extensions)
|
||||
const workflow = parseVideoWorkflow(fields.workflow)
|
||||
const useIdentityRefs = workflow === 'v2' && fields.useIdentityRefs === 'true'
|
||||
|
||||
const { width, height } = resolveOutputSize(fields.aspect, image.data, Number(fields.width), Number(fields.height))
|
||||
const useIdentityRefs = workflow === 'v2' && !textToVideo && !isLtxWorkflow(workflow) && fields.useIdentityRefs === 'true'
|
||||
const { width, height } = resolveOutputSize(
|
||||
fields.aspect,
|
||||
image?.data || Buffer.alloc(0),
|
||||
Number(fields.width),
|
||||
Number(fields.height)
|
||||
)
|
||||
const turbo = fields.turbo !== 'false'
|
||||
const steps = Number(fields.steps || defaultVideoSteps(turbo, workflow))
|
||||
const seed = fields.seed && fields.seed !== 'random'
|
||||
@@ -105,13 +111,13 @@ export default defineEventHandler(async (event) => {
|
||||
: Math.floor(Math.random() * 2_147_483_647)
|
||||
const durationSeconds = parseDuration(fields.duration || '5')
|
||||
const fps = parseFps(fields.fps)
|
||||
const cfg = parseCfg(fields.cfg, turbo)
|
||||
const samplerName = parseSampler(fields.sampler_name)
|
||||
const cfg = parseCfg(fields.cfg, turbo, isLtxWorkflow(workflow))
|
||||
const samplerName = parseSampler(fields.sampler_name, isLtxWorkflow(workflow))
|
||||
const scheduler = parseScheduler(fields.scheduler)
|
||||
const length = frameLength(durationSeconds, fps)
|
||||
const hideThumbnail = fields.hideThumbnail === 'true'
|
||||
const hideInput = fields.hideInput === 'true'
|
||||
const sound = fields.sound !== 'false'
|
||||
const sound = !isLtxWorkflow(workflow) && fields.sound !== 'false'
|
||||
const clipName = (fields.name || '').trim().slice(0, 80)
|
||||
const ownerKey = libraryOwnerKey(event)
|
||||
const library = publicLibrary(event)
|
||||
@@ -124,15 +130,17 @@ export default defineEventHandler(async (event) => {
|
||||
const destFolder = library.folders.find(folder => folder.id === folderId)
|
||||
const folderLocked = Boolean(destFolder?.protected && !destFolder.unlocked)
|
||||
assertFolderExists(event, folderId)
|
||||
const still = await rememberInputStill({
|
||||
ownerKey,
|
||||
folderId,
|
||||
filename: image.filename,
|
||||
data: image.data,
|
||||
width,
|
||||
height,
|
||||
hideInput
|
||||
})
|
||||
const still = image
|
||||
? await rememberInputStill({
|
||||
ownerKey,
|
||||
folderId,
|
||||
filename: image.filename,
|
||||
data: image.data,
|
||||
width,
|
||||
height,
|
||||
hideInput
|
||||
})
|
||||
: null
|
||||
const referenceStillIds: Array<string | null> = [null, null, null, null]
|
||||
if (useIdentityRefs) {
|
||||
for (const [index, ref] of referenceImages.entries()) {
|
||||
@@ -175,7 +183,7 @@ export default defineEventHandler(async (event) => {
|
||||
fps,
|
||||
samplerName,
|
||||
scheduler,
|
||||
thumb: isPipelineFrameFilename(image.filename) ? undefined : image.data,
|
||||
thumb: image && !isPipelineFrameFilename(image.filename) ? image.data : undefined,
|
||||
stillId: still?.id,
|
||||
stillFilename: still?.filename,
|
||||
referenceStillIds,
|
||||
|
||||
Reference in New Issue
Block a user