Add extension-frame refine and lossless anchor options.

Prefer clip anchor PNGs for handoffs, optionally run Bilateral+Flux cleanup before I2V, and save decoded last-frame anchors so sequential extensions accumulate less compression damage.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-09-04 07:55:46 -05:00
co-authored by Cursor
parent 5917d5bb37
commit f41ac7a826
14 changed files with 674 additions and 14 deletions
+33 -3
View File
@@ -7,9 +7,13 @@ import { comfyFilenamePrefix, queuePrompt, uploadImage } from '~/server/utils/co
import { buildWorkflow } from '~/server/utils/workflow'
import { isLtxWorkflow, isTextToVideo, parseVideoWorkflow, workflowForExtension, type VideoWorkflowId } from '~/utils/videoModels'
import { clipVideoPath, clipTitle, deleteRetryDraft, extendTempDir, getClip, nextFamilyPartName, removeExtendTemp, stillPath } from '~/server/utils/library'
import { extractLastFrame, probeHasAudio } from '~/server/utils/ffmpeg'
import { probeHasAudio } from '~/server/utils/ffmpeg'
import { ensureComfyReady } from '~/server/utils/comfyLifecycle'
import { onLiveVideoSettled, onStudioQueueBurstStarted } from '~/server/utils/studioQueue'
import {
refineExtensionHandoffFrame,
resolveExtensionHandoffFrame
} from '~/server/utils/extensionFrame'
import {
finishQueueBurst,
getShotQueue,
@@ -216,6 +220,7 @@ export async function queueMiniMax(
duration: params.duration,
useIdentityRefs: params.useIdentityRefs,
referenceImageNames: params.useIdentityRefs ? referenceNames : [],
saveLosslessAnchor: job.library?.saveLosslessAnchor === true,
...persistLoraFields(params.loraStack || params.loraName)
})
@@ -373,7 +378,12 @@ export async function continueQueuedExtensions(
const parentVideo = parentId ? clipVideoPath(job.library.ownerKey, parentId) : ''
const extractSource = parentVideo && existsSync(parentVideo) ? parentVideo : part1Path
try {
await extractLastFrame(extractSource, framePath)
await resolveExtensionHandoffFrame({
ownerKey: job.library.ownerKey,
sourceClipId: parentId,
sourceVideoPath: extractSource,
destPath: framePath
})
} catch (error) {
const detail = error instanceof Error ? error.message : String(error)
throw new Error(detail.includes('last frame')
@@ -385,10 +395,30 @@ export async function continueQueuedExtensions(
job.library.stopAfterCurrent = true
break
}
const frame = readFileSync(framePath)
let frame = readFileSync(framePath)
if (!frame.length || frame.length < 64) {
throw new Error('Could not extract the last frame for the next extension: the frame file was empty')
}
if (job.library.refineExtensionFrame !== false) {
emitChainJob(job, { type: 'status', message: 'Refining extension handoff frame...', progress: 2 })
try {
frame = await refineExtensionHandoffFrame({
frame,
denoise: job.library.refinementDenoise,
clientId: job.clientId,
jobId: job.id,
onStatus: (message) => emitChainJob(job, { type: 'status', message, progress: 3 })
})
writeFileSync(framePath, frame)
} catch (error) {
const detail = error instanceof Error ? error.message : String(error)
emitChainJob(job, {
type: 'status',
message: `Frame refine skipped (${detail}). Using source frame.`,
progress: 3
})
}
}
job.library.extendPart1Path = part1Path
job.library.prompt = composePromptParts(wrappers.promptPre, ext.prompt, wrappers.promptPost)
job.library.promptMid = ext.prompt