Add LoRA stacks with per-item strengths and named video/image generation presets so setups survive reload without mixing Klein onto MiniMax. Keep LTX Pink Cherry and xAIGen LoRAs on xAIGen, and run Recommend on Beast.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-28 15:24:44 -05:00
co-authored by Cursor
parent 0af3cddb14
commit f8db0d626a
36 changed files with 1767 additions and 246 deletions
+31 -8
View File
@@ -1,6 +1,8 @@
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
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'
export type ShotQueueStatus = 'idle' | 'running' | 'paused' | 'complete' | 'error'
export type ShotSegmentStatus = 'pending' | 'running' | 'complete' | 'error'
@@ -13,6 +15,8 @@ export interface ShotQueueSegment {
clipId?: string
error?: string
permanenceRefs?: import('~/utils/globalLocks').PermanenceRef[]
loraName?: string
loraStack?: LoraStackItem[]
}
export interface ShotQueue {
@@ -46,6 +50,8 @@ export interface ShotQueue {
referenceStillIds?: Array<string | null>
globalLocks?: string
permanenceRefs?: import('~/utils/globalLocks').PermanenceRef[]
loraName?: string
loraStack?: LoraStackItem[]
segments: ShotQueueSegment[]
lastError?: string
}
@@ -163,8 +169,10 @@ export async function createShotQueue(params: {
referenceStillIds?: Array<string | null>
globalLocks?: string
permanenceRefs?: import('~/utils/globalLocks').PermanenceRef[]
initial: { prompt: string; duration: number; permanenceRefs?: import('~/utils/globalLocks').PermanenceRef[] }
extensions: { prompt: string; duration: number; permanenceRefs?: import('~/utils/globalLocks').PermanenceRef[] }[]
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[] }[]
jobId?: string
}): Promise<ShotQueue> {
const now = Date.now()
@@ -198,14 +206,17 @@ export async function createShotQueue(params: {
referenceStillIds: params.referenceStillIds,
globalLocks: params.globalLocks,
permanenceRefs: params.permanenceRefs,
loraName: params.loraName,
loraStack: params.loraStack,
segments: [
{ index: 0, prompt: params.initial.prompt, duration: params.initial.duration, status: 'running', permanenceRefs: params.initial.permanenceRefs },
{ index: 0, prompt: params.initial.prompt, duration: params.initial.duration, status: 'running', permanenceRefs: params.initial.permanenceRefs, ...persistLoraFields(params.initial.loraStack || params.initial.loraName) },
...params.extensions.map((item, index) => ({
index: index + 1,
prompt: item.prompt,
duration: item.duration,
status: 'pending' as const,
permanenceRefs: item.permanenceRefs
permanenceRefs: item.permanenceRefs,
...persistLoraFields(item.loraStack || item.loraName)
}))
]
}
@@ -230,7 +241,7 @@ export async function updateShotQueue(owner: string, id: string, patch: (queue:
export async function patchShotQueueSegments(
owner: string,
id: string,
segments: Array<{ index: number; prompt?: string; duration?: number }>
segments: Array<{ index: number; prompt?: string; duration?: number; loraName?: string; loraStack?: unknown }>
) {
return updateShotQueue(owner, id, (queue) => {
if (queue.status === 'complete') {
@@ -248,6 +259,17 @@ export async function patchShotQueueSegments(
if (typeof next.duration === 'number' && Number.isFinite(next.duration)) {
segment.duration = Math.min(120, Math.max(0.5, Math.round(next.duration * 10) / 10))
}
if ('loraStack' in next || 'loraName' in next) {
const stack = parsePostedLoraStack(next.loraStack ?? next.loraName, 'video')
const fields = persistLoraFields(stack)
if (fields.loraStack) {
segment.loraStack = fields.loraStack
segment.loraName = fields.loraName
} else {
delete segment.loraStack
delete segment.loraName
}
}
}
})
}
@@ -426,10 +448,10 @@ export function listOwnersWithQueues() {
.filter(owner => existsSync(queuesPath(owner)))
}
export function remainingFromQueue(queue: ShotQueue): { prompt: string; duration: number; permanenceRefs?: import('~/utils/globalLocks').PermanenceRef[] }[] {
export function remainingFromQueue(queue: ShotQueue): { prompt: string; duration: number; permanenceRefs?: import('~/utils/globalLocks').PermanenceRef[]; loraName?: string; loraStack?: LoraStackItem[] }[] {
return queue.segments
.filter(segment => segment.index > 0)
.map(segment => ({ prompt: segment.prompt, duration: segment.duration, permanenceRefs: segment.permanenceRefs }))
.map(segment => ({ prompt: segment.prompt, duration: segment.duration, permanenceRefs: segment.permanenceRefs, ...persistLoraFields(segment.loraStack || segment.loraName) }))
}
export function lastCompletedIndex(queue: ShotQueue) {
@@ -443,6 +465,7 @@ export function liveSegmentPrompt(queue: ShotQueue, extensionIndex: number) {
return {
prompt: segment?.prompt || '',
duration: segment?.duration || 5,
permanenceRefs: segment?.permanenceRefs
permanenceRefs: segment?.permanenceRefs,
...persistLoraFields(segment?.loraStack || segment?.loraName)
}
}