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:
@@ -1,4 +1,5 @@
|
||||
import { addStudioJob, kickStudioQueue, listStudioJobs } from '~/server/utils/studioQueue'
|
||||
import { parsePostedLoraStack, listStudioLoras, persistLoraFields } from '~/server/utils/loras'
|
||||
import { comfyConfigured } from '~/server/utils/comfy'
|
||||
import { imageDimensions } from '~/server/utils/resolution'
|
||||
|
||||
@@ -81,6 +82,8 @@ export default defineEventHandler(async (event) => {
|
||||
: Math.floor(Math.random() * 2_147_483_647)
|
||||
const size = imageDimensions(image.data)
|
||||
const clipName = (fields.name || '').trim().slice(0, 80)
|
||||
await listStudioLoras()
|
||||
const loraStack = parsePostedLoraStack(fields.loraStack || fields.lora, 'image')
|
||||
|
||||
const still = await rememberInputStill({
|
||||
ownerKey,
|
||||
@@ -137,7 +140,8 @@ export default defineEventHandler(async (event) => {
|
||||
negative: (fields.negative || '').trim(),
|
||||
passes,
|
||||
referenceStillId: savedRef?.id,
|
||||
referenceStillFilename: savedRef?.filename
|
||||
referenceStillFilename: savedRef?.filename,
|
||||
...persistLoraFields(loraStack)
|
||||
}
|
||||
})
|
||||
await kickStudioQueue()
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { copyFileSync, existsSync, readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { composeShotPrompt } from '~/utils/globalLocks'
|
||||
import { parsePostedLoraStack, listStudioLoras, persistLoraFields } from '~/server/utils/loras'
|
||||
import { readLoraStack } from '~/utils/loras'
|
||||
import { defaultVideoSteps, isLtxWorkflow, LTX_DISABLED_MESSAGE, ltxWorkflowEnabled, parseVideoWorkflow } from '~/utils/videoModels'
|
||||
|
||||
const SAMPLERS = new Set(['res_multistep', 'euler', 'dpmpp_2m'])
|
||||
const SCHEDULERS = new Set(['simple', 'ddim_uniform', 'sgm_uniform'])
|
||||
@@ -39,7 +42,7 @@ function parseScheduler(raw: unknown) {
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody<{ clipId?: string; prompt?: string; duration?: number }>(event).catch(() => ({}))
|
||||
const body = await readBody<{ clipId?: string; prompt?: string; duration?: number; lora?: unknown; loraStack?: unknown }>(event).catch(() => ({}))
|
||||
const clipId = String(body?.clipId || '').trim()
|
||||
const prompt = String(body?.prompt || '').trim()
|
||||
if (!clipId) {
|
||||
@@ -66,13 +69,21 @@ export default defineEventHandler(async (event) => {
|
||||
shotIndex: (source.chainIndex || 0) + 1,
|
||||
familyRefs: source.permanenceRefs
|
||||
})
|
||||
const workflow = parseVideoWorkflow(source.workflow)
|
||||
if (isLtxWorkflow(workflow) && !ltxWorkflowEnabled()) {
|
||||
throw createError({ statusCode: 400, statusMessage: LTX_DISABLED_MESSAGE })
|
||||
}
|
||||
const turbo = source.turbo !== false
|
||||
const fps = parseFps(source.fps)
|
||||
const cfg = parseCfg(source.cfg, turbo)
|
||||
const samplerName = parseSampler(source.samplerName)
|
||||
const scheduler = parseScheduler(source.scheduler)
|
||||
const steps = Number(source.steps || defaultVideoSteps(turbo, parseVideoWorkflow(source.workflow)))
|
||||
const steps = Number(source.steps || defaultVideoSteps(turbo, workflow))
|
||||
const seed = Math.floor(Math.random() * 2_147_483_647)
|
||||
await listStudioLoras()
|
||||
const postedStack = parsePostedLoraStack(body?.loraStack ?? body?.lora, 'video')
|
||||
const loraStack = postedStack.length ? postedStack : parsePostedLoraStack(readLoraStack(source), 'video')
|
||||
const loraFields = persistLoraFields(loraStack)
|
||||
const length = frameLength(durationSeconds, fps)
|
||||
const width = source.width || 1344
|
||||
const height = source.height || 768
|
||||
@@ -109,9 +120,10 @@ export default defineEventHandler(async (event) => {
|
||||
familyId: source.familyId || crypto.randomUUID(),
|
||||
parentClipId: source.id,
|
||||
chainIndex: (source.chainIndex || 0) + 1,
|
||||
workflow: parseVideoWorkflow(source.workflow),
|
||||
workflow,
|
||||
globalLocks: source.globalLocks,
|
||||
permanenceRefs: source.permanenceRefs
|
||||
permanenceRefs: source.permanenceRefs,
|
||||
...loraFields
|
||||
}
|
||||
|
||||
emitJob(job, { type: 'status', message: 'Extracting last frame & stitching extension...', progress: 2 })
|
||||
@@ -130,7 +142,8 @@ export default defineEventHandler(async (event) => {
|
||||
cfg,
|
||||
fps,
|
||||
samplerName,
|
||||
scheduler
|
||||
scheduler,
|
||||
...loraFields
|
||||
}).catch((error) => {
|
||||
removeExtendTemp(job.library?.extendTmpDir)
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
@@ -169,6 +182,8 @@ async function runExtend(
|
||||
fps: number
|
||||
samplerName: string
|
||||
scheduler: string
|
||||
loraName?: string
|
||||
loraStack?: import('~/utils/loras').LoraStackItem[]
|
||||
}
|
||||
) {
|
||||
copyFileSync(params.sourcePath, params.part1Path)
|
||||
@@ -218,7 +233,8 @@ async function runExtend(
|
||||
filenamePrefix: comfyFilenamePrefix(),
|
||||
sound,
|
||||
workflow: job.library?.workflow,
|
||||
duration: job.library?.duration
|
||||
duration: job.library?.duration,
|
||||
loraStack: params.loraStack || job.library?.loraStack || params.loraName || job.library?.loraName
|
||||
})
|
||||
|
||||
const queued = await queuePrompt(graph, job.clientId)
|
||||
@@ -254,7 +270,9 @@ async function runExtend(
|
||||
workflow: job.library.workflow,
|
||||
duration: job.library.duration,
|
||||
globalLocks: job.library.globalLocks,
|
||||
permanenceRefs: job.library.permanenceRefs
|
||||
permanenceRefs: job.library.permanenceRefs,
|
||||
loraName: job.library.loraName,
|
||||
loraStack: job.library.loraStack
|
||||
})
|
||||
}
|
||||
emitJob(job, { type: 'status', message: 'Job queued on ComfyUI', progress: 8 })
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { addStudioJob, kickStudioQueue, listStudioJobs } from '~/server/utils/studioQueue'
|
||||
import { defaultVideoSteps, isLtxWorkflow, isTextToVideo, parseVideoWorkflow } from '~/utils/videoModels'
|
||||
import { listStudioLoras, parsePostedLoraStack, parseShotLoraStacks, persistLoraFields } from '~/server/utils/loras'
|
||||
import type { LoraStackItem } from '~/utils/loras'
|
||||
import { defaultVideoSteps, isLtxWorkflow, isTextToVideo, LTX_DISABLED_MESSAGE, ltxWorkflowEnabled, parseVideoWorkflow } from '~/utils/videoModels'
|
||||
import { allowIdentityRefs, normalizePermanenceRefs, resolveGlobalLocks, type PermanenceRef } from '~/utils/globalLocks'
|
||||
|
||||
function parseDuration(raw: unknown) {
|
||||
@@ -13,18 +15,23 @@ function parseExtendDuration(raw: unknown) {
|
||||
}
|
||||
|
||||
function parseExtensions(raw: string | undefined) {
|
||||
if (!raw) return [] as { prompt: string; duration: number; permanenceRefs?: PermanenceRef[] }[]
|
||||
if (!raw) return [] as { prompt: string; duration: number; permanenceRefs?: PermanenceRef[]; loraName?: string; loraStack?: LoraStackItem[] }[]
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
if (!Array.isArray(parsed)) return []
|
||||
return parsed
|
||||
.map((item: { prompt?: unknown; duration?: unknown; permanenceRefs?: unknown }) => ({
|
||||
prompt: String(item?.prompt || '').trim(),
|
||||
duration: parseExtendDuration(item?.duration),
|
||||
permanenceRefs: normalizePermanenceRefs(item?.permanenceRefs)
|
||||
}))
|
||||
.map((item: { prompt?: unknown; duration?: unknown; permanenceRefs?: unknown; loraName?: unknown; loraStack?: unknown }) => {
|
||||
const loraStack = parsePostedLoraStack(item?.loraStack ?? item?.loraName, 'video')
|
||||
return {
|
||||
prompt: String(item?.prompt || '').trim(),
|
||||
duration: parseExtendDuration(item?.duration),
|
||||
permanenceRefs: normalizePermanenceRefs(item?.permanenceRefs),
|
||||
...persistLoraFields(loraStack)
|
||||
}
|
||||
})
|
||||
.filter(item => item.prompt)
|
||||
} catch {
|
||||
} catch (error) {
|
||||
if (error && typeof error === 'object' && 'statusCode' in error) throw error
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -101,7 +108,11 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
}
|
||||
|
||||
await listStudioLoras()
|
||||
const workflow = parseVideoWorkflow(fields.workflow)
|
||||
if (isLtxWorkflow(workflow) && !ltxWorkflowEnabled()) {
|
||||
throw createError({ statusCode: 400, statusMessage: LTX_DISABLED_MESSAGE })
|
||||
}
|
||||
const textToVideo = isTextToVideo(workflow)
|
||||
if (!image && !textToVideo) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'An initial image is required' })
|
||||
@@ -116,10 +127,16 @@ export default defineEventHandler(async (event) => {
|
||||
const extensions = parseExtensions(fields.extensions)
|
||||
const permanenceRefs = parseFamilyPermanenceRefs(fields.permanenceRefs)
|
||||
const shotPermanenceRefs = parseShotPermanenceRefs(fields.shotPermanenceRefs, 1 + extensions.length)
|
||||
const defaultStack = parsePostedLoraStack(fields.loraStack || fields.lora, 'video')
|
||||
const shotLoraStacks = parseShotLoraStacks(fields.shotLoraStacks || fields.shotLoras, 1 + extensions.length)
|
||||
if (defaultStack.length && !shotLoraStacks[0].length) shotLoraStacks[0] = defaultStack
|
||||
for (const [index, item] of extensions.entries()) {
|
||||
if (!shotPermanenceRefs[index + 1]?.length && item.permanenceRefs?.length) {
|
||||
shotPermanenceRefs[index + 1] = item.permanenceRefs
|
||||
}
|
||||
const shotStack = shotLoraStacks[index + 1]
|
||||
if (!shotStack.length && item.loraStack?.length) shotLoraStacks[index + 1] = item.loraStack
|
||||
else if (shotStack.length) Object.assign(item, persistLoraFields(shotStack))
|
||||
}
|
||||
const requestedIdentity = workflow === 'v2' && !textToVideo && !isLtxWorkflow(workflow) && fields.useIdentityRefs === 'true'
|
||||
const useIdentityRefs = allowIdentityRefs(requestedIdentity, permanenceRefs, globalLocks, shotPermanenceRefs)
|
||||
@@ -216,7 +233,10 @@ export default defineEventHandler(async (event) => {
|
||||
queueAutoRun,
|
||||
globalLocks: globalLocks || undefined,
|
||||
permanenceRefs: permanenceRefs.length ? permanenceRefs : undefined,
|
||||
shotPermanenceRefs: shotPermanenceRefs.some(item => item.length) ? shotPermanenceRefs : undefined
|
||||
shotPermanenceRefs: shotPermanenceRefs.some(item => item.length) ? shotPermanenceRefs : undefined,
|
||||
...persistLoraFields(defaultStack),
|
||||
shotLoras: shotLoraStacks.some(stack => stack.length) ? shotLoraStacks.map(stack => stack[0]?.name || '') : undefined,
|
||||
shotLoraStacks: shotLoraStacks.some(stack => stack.length) ? shotLoraStacks : undefined
|
||||
}
|
||||
})
|
||||
await kickStudioQueue()
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { listGenerationPresets } from '~/server/utils/generationPresets'
|
||||
import { parseGenerationPresetKind } from '~/utils/generationPresets'
|
||||
|
||||
export default defineEventHandler((event) => {
|
||||
const { owner } = assertLibraryOwner(event)
|
||||
const kind = parseGenerationPresetKind(getQuery(event).kind)
|
||||
return {
|
||||
presets: listGenerationPresets(owner, kind)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import { listGenerationPresets, saveGenerationPreset } from '~/server/utils/generationPresets'
|
||||
import { parseGenerationPresetKind } from '~/utils/generationPresets'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { owner } = assertLibraryOwner(event)
|
||||
const body = await readBody<{
|
||||
name?: string
|
||||
kind?: string
|
||||
loraStack?: unknown
|
||||
settings?: unknown
|
||||
overwrite?: boolean
|
||||
}>(event).catch(() => ({}))
|
||||
const kind = parseGenerationPresetKind(body?.kind)
|
||||
const preset = await saveGenerationPreset(owner, {
|
||||
name: body?.name || '',
|
||||
kind,
|
||||
loraStack: body?.loraStack,
|
||||
settings: body?.settings,
|
||||
overwrite: body?.overwrite === true
|
||||
})
|
||||
return {
|
||||
preset,
|
||||
presets: listGenerationPresets(owner, kind)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import { deleteGenerationPreset, listGenerationPresets } from '~/server/utils/generationPresets'
|
||||
import { parseGenerationPresetKind } from '~/utils/generationPresets'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { owner } = assertLibraryOwner(event)
|
||||
const id = String(getRouterParam(event, 'id') || '')
|
||||
const kind = parseGenerationPresetKind(getQuery(event).kind)
|
||||
await deleteGenerationPreset(owner, id)
|
||||
return {
|
||||
ok: true,
|
||||
presets: listGenerationPresets(owner, kind)
|
||||
}
|
||||
})
|
||||
+19
-15
@@ -1,14 +1,15 @@
|
||||
import { checkComfyHttp, fetchQueue } from '~/server/utils/comfyLifecycle'
|
||||
import { comfyConfigured } from '~/server/utils/comfy'
|
||||
import { imageComfyConfigured, getBeastImageHost, getSidecarImageHost } from '~/server/utils/imageComfy'
|
||||
import { imageComfyConfigured, getBeastImageHost, getSidecarImageHost, sameImageHost } from '~/server/utils/imageComfy'
|
||||
import { checkImageComfyHttp, fetchQueueAt } from '~/server/utils/imageComfyLifecycle'
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
const videoConfigured = comfyConfigured()
|
||||
const imageConfigured = imageComfyConfigured()
|
||||
const beastHost = getBeastImageHost()
|
||||
const sidecarHost = getSidecarImageHost()
|
||||
const [comfy, queue, beast, beastQueue, sidecar, sidecarQueue] = await Promise.all([
|
||||
const promptHost = getSidecarImageHost()
|
||||
const promptIsBeast = Boolean(promptHost && beastHost && sameImageHost(promptHost, beastHost))
|
||||
const [comfy, queue, beast, beastQueue, prompt, promptQueue] = await Promise.all([
|
||||
videoConfigured
|
||||
? checkComfyHttp().catch(() => ({ ok: false, host: '', stats: null }))
|
||||
: Promise.resolve({ ok: false, host: '', stats: null }),
|
||||
@@ -17,17 +18,20 @@ export default defineEventHandler(async () => {
|
||||
? checkImageComfyHttp(2500, beastHost).catch(() => ({ ok: false, host: beastHost, stats: null }))
|
||||
: Promise.resolve({ ok: false, host: beastHost, stats: null }),
|
||||
beastHost ? fetchQueueAt(beastHost) : Promise.resolve({ running: 0, pending: 0 }),
|
||||
sidecarHost
|
||||
? checkImageComfyHttp(2500, sidecarHost).catch(() => ({ ok: false, host: sidecarHost, stats: null }))
|
||||
: Promise.resolve({ ok: false, host: sidecarHost, stats: null }),
|
||||
sidecarHost ? fetchQueueAt(sidecarHost) : Promise.resolve({ running: 0, pending: 0 })
|
||||
promptHost && !promptIsBeast
|
||||
? checkImageComfyHttp(2500, promptHost).catch(() => ({ ok: false, host: promptHost, stats: null }))
|
||||
: Promise.resolve({ ok: false, host: promptHost, stats: null }),
|
||||
promptHost && !promptIsBeast ? fetchQueueAt(promptHost) : Promise.resolve({ running: 0, pending: 0 })
|
||||
])
|
||||
const busy = Boolean(comfy.ok && queue.running > 0)
|
||||
const beastBusy = Boolean(beast.ok && beastQueue.running > 0)
|
||||
const sidecarBusy = Boolean(sidecar.ok && sidecarQueue.running > 0)
|
||||
const promptOk = promptIsBeast ? Boolean(beast.ok || comfy.ok) : Boolean(prompt.ok)
|
||||
const promptBusy = promptIsBeast ? (beastBusy || busy) : Boolean(prompt.ok && promptQueue.running > 0)
|
||||
const imageOk = Boolean(beast.ok || comfy.ok)
|
||||
const imageBusy = Boolean(imageConfigured && (beastBusy || busy))
|
||||
const preferredHost = beastHost || comfy.host || ''
|
||||
const promptQueueRunning = promptIsBeast ? ((beastQueue.running || queue.running) || 0) : (promptQueue.running || 0)
|
||||
const promptQueuePending = promptIsBeast ? ((beastQueue.pending || queue.pending) || 0) : (promptQueue.pending || 0)
|
||||
return {
|
||||
ok: true,
|
||||
service: useRuntimeConfig().public.instanceName || 'aigen',
|
||||
@@ -55,13 +59,13 @@ export default defineEventHandler(async () => {
|
||||
host: preferredHost
|
||||
},
|
||||
sidecar: {
|
||||
label: 'Sidecar',
|
||||
configured: Boolean(sidecarHost),
|
||||
ok: Boolean(sidecar.ok),
|
||||
busy: sidecarBusy,
|
||||
host: sidecarHost,
|
||||
queueRunning: sidecarQueue.running || 0,
|
||||
queuePending: sidecarQueue.pending || 0
|
||||
label: 'Qwen VL',
|
||||
configured: Boolean(promptHost),
|
||||
ok: promptOk,
|
||||
busy: promptBusy,
|
||||
host: promptHost,
|
||||
queueRunning: promptQueueRunning,
|
||||
queuePending: promptQueuePending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { listStudioLoras } from '~/server/utils/loras'
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
const listed = await listStudioLoras()
|
||||
return {
|
||||
image: listed.image,
|
||||
video: listed.video
|
||||
}
|
||||
})
|
||||
@@ -5,7 +5,7 @@ export default defineEventHandler(async (event) => {
|
||||
if (!promptComfyConfigured()) {
|
||||
throw createError({
|
||||
statusCode: 503,
|
||||
statusMessage: 'Sidecar Qwen VL is not configured. Set IMAGE_COMFY_HOST.'
|
||||
statusMessage: 'Qwen VL is not configured. Set COMFY_HOST or IMAGE_COMFY_HOST to Beast.'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getShotQueue, patchShotQueueSegments, updateShotQueue, summarizeQueue } from '~/server/utils/shotQueue'
|
||||
import { listStudioLoras } from '~/server/utils/loras'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { owner } = assertLibraryOwner(event)
|
||||
@@ -6,10 +7,11 @@ export default defineEventHandler(async (event) => {
|
||||
const body = await readBody<{
|
||||
name?: string
|
||||
autoRun?: boolean
|
||||
segments?: Array<{ index: number; prompt?: string; duration?: number }>
|
||||
segments?: Array<{ index: number; prompt?: string; duration?: number; loraName?: string; loraStack?: unknown }>
|
||||
}>().catch(() => ({}))
|
||||
|
||||
if (Array.isArray(body?.segments) && body.segments.length) {
|
||||
await listStudioLoras()
|
||||
await patchShotQueueSegments(owner, id, body.segments)
|
||||
}
|
||||
if (typeof body?.name === 'string' || typeof body?.autoRun === 'boolean') {
|
||||
|
||||
@@ -23,8 +23,13 @@ export default defineEventHandler((event) => {
|
||||
]
|
||||
: undefined)
|
||||
: [
|
||||
{ prompt: job.payload.prompt, duration: job.payload.duration },
|
||||
...(job.payload.extensions || [])
|
||||
{ prompt: job.payload.prompt, duration: job.payload.duration, loraName: job.payload.shotLoras?.[0] || job.payload.loraName, loraStack: job.payload.shotLoraStacks?.[0] || job.payload.loraStack },
|
||||
...(job.payload.extensions || []).map((item, index) => ({
|
||||
prompt: item.prompt,
|
||||
duration: item.duration,
|
||||
loraName: item.loraName || job.payload.shotLoras?.[index + 1],
|
||||
loraStack: item.loraStack || job.payload.shotLoraStacks?.[index + 1]
|
||||
}))
|
||||
]
|
||||
return {
|
||||
...summarizeStudioJob(job),
|
||||
|
||||
Reference in New Issue
Block a user