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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user