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:
Towsty
2026-08-27 19:38:53 -05:00
co-authored by Cursor
parent 2bebeea526
commit 40cd17f35c
15 changed files with 676 additions and 120 deletions
+24 -15
View File
@@ -5,6 +5,7 @@ import { pendingFromJob, remainingAfterCurrentShot, writePendingJob } from '~/se
import { emitChainJob, waitForComfySocket, watchComfyJob } from '~/server/utils/watch'
import { comfyFilenamePrefix, queuePrompt, uploadImage } from '~/server/utils/comfy'
import { buildWorkflow } from '~/server/utils/workflow'
import { isLtxWorkflow, isTextToVideo, parseVideoWorkflow, workflowForExtension, type VideoWorkflowId } from '~/utils/videoModels'
import { clipVideoPath, clipTitle, deleteRetryDraft, extendTempDir, getClip, nextClipPartName, removeExtendTemp, stillPath } from '~/server/utils/library'
import { extractLastFrame, probeHasAudio } from '~/server/utils/ffmpeg'
import { ensureComfyReady } from '~/server/utils/comfyLifecycle'
@@ -23,7 +24,7 @@ export type ChainImage = { filename: string; data: Buffer; type?: string }
type VideoChainParams = {
prompt: string
image: ChainImage
image?: ChainImage | null
width: number
height: number
steps: number
@@ -36,7 +37,7 @@ type VideoChainParams = {
samplerName: string
scheduler: string
extensions: { prompt: string; duration: number }[]
workflow: 'v1' | 'v2'
workflow: VideoWorkflowId
duration: number
useIdentityRefs: boolean
referenceImages: Array<ChainImage | null>
@@ -76,7 +77,8 @@ function paramsFromJob(job: Job): VideoChainParams {
throw new Error('Cannot continue the shot chain: job metadata is missing')
}
const image = loadStillImage(library.ownerKey, library.stillId, library.stillFilename)
if (!image?.data.length) {
const workflow = parseVideoWorkflow(library.workflow)
if (!image?.data.length && !isTextToVideo(workflow)) {
throw new Error('Cannot continue the shot chain: the start still is missing from the library')
}
const referenceImages: Array<ChainImage | null> = [null, null, null, null]
@@ -101,7 +103,7 @@ function paramsFromJob(job: Job): VideoChainParams {
samplerName: library.samplerName || 'res_multistep',
scheduler: library.scheduler || 'simple',
extensions: library.extensions || [],
workflow: library.workflow || 'v1',
workflow,
duration,
useIdentityRefs: library.useIdentityRefs === true,
referenceImages
@@ -121,14 +123,21 @@ export async function queueMiniMax(
const done = watchComfyJob(job, { persist: params.persist })
job.status = 'uploading'
const chainIndex = job.library?.chainIndex || 0
const uploading = params.useIdentityRefs
? (chainIndex > 0 ? 'Uploading identity stills for next shot...' : 'Uploading image to ComfyUI...')
: (chainIndex > 0 ? 'Uploading last frame to ComfyUI...' : 'Uploading image to ComfyUI...')
const queueing = (job.library?.chainIndex || 0) > 0
? 'Queueing extension on MiniMax H3...'
: 'Queueing MiniMax H3 job...'
const graphId = chainIndex > 0 ? workflowForExtension(params.workflow) : params.workflow
const engineName = isLtxWorkflow(graphId) ? 'LTX-2.3' : 'MiniMax H3'
const hasImage = Boolean(params.image?.data?.length)
const uploading = !hasImage
? `Queueing ${engineName} text-to-video…`
: params.useIdentityRefs
? (chainIndex > 0 ? 'Uploading identity stills for next shot...' : 'Uploading image to ComfyUI...')
: (chainIndex > 0 ? 'Uploading last frame to ComfyUI...' : 'Uploading image to ComfyUI...')
const queueing = chainIndex > 0
? `Queueing extension on ${engineName}...`
: `Queueing ${engineName} job...`
emitChainJob(job, { type: 'status', message: uploading, progress: 4 })
const uploaded = await uploadImage(params.image, job.id)
const uploaded = hasImage && params.image
? await uploadImage(params.image, job.id)
: { name: '', subfolder: '' }
const referenceNames: string[] = ['', '', '', '']
if (params.useIdentityRefs) {
for (const [index, ref] of (params.referenceImages || []).entries()) {
@@ -161,9 +170,9 @@ export async function queueMiniMax(
fps: params.fps,
samplerName: params.samplerName,
scheduler: params.scheduler,
filenamePrefix: comfyFilenamePrefix(),
sound: params.sound,
workflow: params.workflow,
filenamePrefix: isLtxWorkflow(graphId) ? 'video/LTX23' : comfyFilenamePrefix(),
sound: params.sound && !isLtxWorkflow(graphId),
workflow: graphId,
duration: params.duration,
useIdentityRefs: params.useIdentityRefs,
referenceImageNames: params.useIdentityRefs ? referenceNames : []
@@ -317,7 +326,7 @@ export async function continueQueuedExtensions(
samplerName: params.samplerName,
scheduler: params.scheduler,
persist,
workflow: params.workflow,
workflow: workflowForExtension(params.workflow),
duration: ext.duration,
useIdentityRefs: params.useIdentityRefs,
referenceImages: params.useIdentityRefs ? params.referenceImages : []