Move library collections as a group and map v2 identity refs.

Collapsed collection actions now move, rename, delete, and rerun every part so chains stay together. Identity mode follows the v2u3 Ref2VA picture wiring.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-26 22:56:37 -05:00
co-authored by Cursor
parent f54e8da857
commit e334cb7a7a
28 changed files with 1125 additions and 293 deletions
+33 -20
View File
@@ -83,13 +83,20 @@ export default defineEventHandler(async (event) => {
const fields: Record<string, string> = {}
let image: { filename: string; data: Buffer; type?: string } | null = null
const referenceImages: { filename: string; data: Buffer; type?: string }[] = []
const referenceImages: Array<{ filename: string; data: Buffer; type?: string } | null> = [null, null, null, null]
let compactRefSlot = 0
for (const part of form) {
if (part.name === 'image' && part.filename && part.data?.length) {
image = { filename: part.filename, data: part.data, type: part.type }
} else if (part.name && /^refImage(\d)$/.test(part.name) && part.filename && part.data?.length) {
const slot = Number(part.name.replace('refImage', ''))
if (slot >= 0 && slot < 4) {
referenceImages[slot] = { filename: part.filename, data: part.data, type: part.type }
}
} else if (part.name === 'refImage' && part.filename && part.data?.length) {
if (referenceImages.length < 4) {
referenceImages.push({ filename: part.filename, data: part.data, type: part.type })
if (compactRefSlot < 4) {
referenceImages[compactRefSlot] = { filename: part.filename, data: part.data, type: part.type }
compactRefSlot += 1
}
} else if (part.name && part.data) {
fields[part.name] = part.data.toString('utf8')
@@ -105,7 +112,7 @@ export default defineEventHandler(async (event) => {
}
const extensions = parseExtensions(fields.extensions)
const workflow = parseVideoWorkflow(fields.workflow)
const useIdentityRefs = workflow === 'v2' && fields.useIdentityRefs === 'true' && referenceImages.length > 0
const useIdentityRefs = workflow === 'v2' && fields.useIdentityRefs === 'true'
const { width, height } = resolveOutputSize(fields.aspect, image.data, Number(fields.width), Number(fields.height))
const turbo = fields.turbo !== 'false'
@@ -134,15 +141,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 saveStill({
ownerKey,
folderId,
filename: image.filename,
data: image.data,
width,
height,
hideInput
})
const still = isPipelineFrameFilename(image.filename)
? null
: await saveStill({
ownerKey,
folderId,
filename: image.filename,
data: image.data,
width,
height,
hideInput
})
const chainTotal = 1 + extensions.length
const job = createJob()
@@ -167,9 +176,9 @@ export default defineEventHandler(async (event) => {
fps,
samplerName,
scheduler,
thumb: image.data,
stillId: still.id,
stillFilename: still.filename,
thumb: isPipelineFrameFilename(image.filename) ? undefined : image.data,
stillId: still?.id,
stillFilename: still?.filename,
duration: durationSeconds,
sound,
draftId: (fields.draftId || '').trim() || undefined,
@@ -285,7 +294,7 @@ type GenerateParams = {
workflow: 'v1' | 'v2'
duration: number
useIdentityRefs: boolean
referenceImages: { filename: string; data: Buffer; type?: string }[]
referenceImages: Array<{ filename: string; data: Buffer; type?: string } | null>
}
async function queueMiniMax(
@@ -308,15 +317,16 @@ async function queueMiniMax(
: 'Queueing MiniMax H3 job...'
emitChainJob(job, { type: 'status', message: uploading, progress: 4 })
const uploaded = await uploadImage(params.image, job.id)
const referenceNames: string[] = []
const referenceNames: string[] = ['', '', '', '']
const chainIndex = job.library?.chainIndex || 0
if (chainIndex === 0) {
for (const [index, ref] of (params.referenceImages || []).entries()) {
if (!ref?.data?.length) continue
const next = await uploadImage({
...ref,
filename: `ref${index + 1}_${ref.filename || 'identity.png'}`
}, job.id)
referenceNames.push(next.name)
referenceNames[index] = next.name
}
}
if (job.library) {
@@ -378,7 +388,9 @@ async function queueMiniMax(
extendPart1Path: job.library.extendPart1Path,
familyId: job.library.familyId,
parentClipId: job.library.parentClipId,
chainIndex: job.library.chainIndex
chainIndex: job.library.chainIndex,
stillId: job.library.stillId,
sound: job.library.sound
})
}
emitChainJob(job, { type: 'status', message: 'Job queued on ComfyUI', progress: 8 })
@@ -430,6 +442,7 @@ async function runGeneration(
job.library.chainStep = i + 2
job.library.chainLabel = `Extension ${i + 1}`
job.library.extendPart1Path = undefined
job.library.thumb = undefined
emitChainJob(job, { type: 'status', message: 'Waiting 3s buffer...', progress: 1 })
await sleep(3000)