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:
@@ -1,4 +1,4 @@
|
||||
import { copyFileSync, existsSync, readFileSync } from 'node:fs'
|
||||
import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { composeShotPrompt } from '~/utils/globalLocks'
|
||||
import { composePromptParts } from '~/utils/promptParts'
|
||||
@@ -16,12 +16,22 @@ import {
|
||||
nextFamilyPartName,
|
||||
removeExtendTemp
|
||||
} from '~/server/utils/library'
|
||||
import { extractLastFrame, probeHasAudio } from '~/server/utils/ffmpeg'
|
||||
import { probeHasAudio } from '~/server/utils/ffmpeg'
|
||||
import { ensureComfyReady } from '~/server/utils/comfyLifecycle'
|
||||
import { waitForComfySocket, watchComfyJob } from '~/server/utils/watch'
|
||||
import { comfyFilenamePrefix, queuePrompt, uploadImage } from '~/server/utils/comfy'
|
||||
import { buildWorkflow } from '~/server/utils/workflow'
|
||||
import { writePendingJob } from '~/server/utils/pending'
|
||||
import {
|
||||
refineExtensionHandoffFrame,
|
||||
resolveExtensionHandoffFrame
|
||||
} from '~/server/utils/extensionFrame'
|
||||
import {
|
||||
clampRefinementDenoise,
|
||||
parseRefineExtensionFrame,
|
||||
parseSaveLosslessAnchor,
|
||||
REFINEMENT_DENOISE_DEFAULT
|
||||
} from '~/utils/extensionFrame'
|
||||
|
||||
const SAMPLERS = new Set(['res_multistep', 'euler', 'dpmpp_2m'])
|
||||
const SCHEDULERS = new Set(['simple', 'ddim_uniform', 'sgm_uniform'])
|
||||
@@ -63,6 +73,9 @@ export async function beginExtendFromClip(params: {
|
||||
loraStack?: unknown
|
||||
folderLocked?: boolean
|
||||
name?: string
|
||||
refineExtensionFrame?: boolean
|
||||
saveLosslessAnchor?: boolean
|
||||
refinementDenoise?: number
|
||||
}) {
|
||||
const source = getClip(params.ownerKey, params.clipId)
|
||||
const sourcePath = clipVideoPath(params.ownerKey, source.id)
|
||||
@@ -111,6 +124,9 @@ export async function beginExtendFromClip(params: {
|
||||
const width = source.width || 1344
|
||||
const height = source.height || 768
|
||||
const clipName = String(params.name || '').trim() || nextFamilyPartName(params.ownerKey, source)
|
||||
const refineExtensionFrame = parseRefineExtensionFrame(params.refineExtensionFrame, true)
|
||||
const saveLosslessAnchor = parseSaveLosslessAnchor(params.saveLosslessAnchor, false)
|
||||
const refinementDenoise = clampRefinementDenoise(params.refinementDenoise, REFINEMENT_DENOISE_DEFAULT)
|
||||
|
||||
const job = createJob()
|
||||
job.kind = 'video'
|
||||
@@ -149,6 +165,9 @@ export async function beginExtendFromClip(params: {
|
||||
workflow,
|
||||
globalLocks: source.globalLocks,
|
||||
permanenceRefs: source.permanenceRefs,
|
||||
refineExtensionFrame,
|
||||
saveLosslessAnchor,
|
||||
refinementDenoise,
|
||||
...loraFields
|
||||
}
|
||||
|
||||
@@ -169,6 +188,9 @@ export async function beginExtendFromClip(params: {
|
||||
fps,
|
||||
samplerName,
|
||||
scheduler,
|
||||
refineExtensionFrame,
|
||||
saveLosslessAnchor,
|
||||
refinementDenoise,
|
||||
...loraFields
|
||||
}).catch((error) => {
|
||||
removeExtendTemp(job.library?.extendTmpDir)
|
||||
@@ -198,13 +220,21 @@ async function runExtend(
|
||||
fps: number
|
||||
samplerName: string
|
||||
scheduler: string
|
||||
refineExtensionFrame?: boolean
|
||||
saveLosslessAnchor?: boolean
|
||||
refinementDenoise?: number
|
||||
loraName?: string
|
||||
loraStack?: import('~/utils/loras').LoraStackItem[]
|
||||
}
|
||||
) {
|
||||
copyFileSync(params.sourcePath, params.part1Path)
|
||||
await extractLastFrame(params.sourcePath, params.framePath)
|
||||
const frame = readFileSync(params.framePath)
|
||||
const handoff = await resolveExtensionHandoffFrame({
|
||||
ownerKey: job.library!.ownerKey,
|
||||
sourceClipId: job.library?.extendSourceClipId,
|
||||
sourceVideoPath: params.sourcePath,
|
||||
destPath: params.framePath
|
||||
})
|
||||
let frame = readFileSync(handoff.path)
|
||||
if (!frame.length || frame.length < 64) {
|
||||
throw new Error('Could not extract the last frame of the clip being extended')
|
||||
}
|
||||
@@ -223,8 +253,36 @@ async function runExtend(
|
||||
})
|
||||
|
||||
const done = watchComfyJob(job)
|
||||
if (params.refineExtensionFrame !== false) {
|
||||
emitJob(job, {
|
||||
type: 'status',
|
||||
message: handoff.source === 'anchor'
|
||||
? 'Refining lossless anchor frame...'
|
||||
: 'Refining extracted handoff frame...',
|
||||
progress: 3
|
||||
})
|
||||
try {
|
||||
frame = await refineExtensionHandoffFrame({
|
||||
frame,
|
||||
denoise: params.refinementDenoise,
|
||||
seed: params.seed,
|
||||
clientId: job.clientId,
|
||||
jobId: job.id,
|
||||
onStatus: (message) => emitJob(job, { type: 'status', message, progress: 4 })
|
||||
})
|
||||
writeFileSync(params.framePath, frame)
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error)
|
||||
emitJob(job, {
|
||||
type: 'status',
|
||||
message: `Frame refine skipped (${detail}). Using source frame.`,
|
||||
progress: 4
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
job.status = 'uploading'
|
||||
emitJob(job, { type: 'status', message: 'Uploading last frame to ComfyUI...', progress: 4 })
|
||||
emitJob(job, { type: 'status', message: 'Uploading last frame to ComfyUI...', progress: 5 })
|
||||
const uploaded = await uploadImage({ filename: 'last_frame.png', data: frame, type: 'image/png' }, job.id)
|
||||
if (job.library) {
|
||||
job.library.imageName = uploaded.name
|
||||
@@ -251,7 +309,8 @@ async function runExtend(
|
||||
sound,
|
||||
workflow: job.library?.workflow,
|
||||
duration: job.library?.duration,
|
||||
loraStack: params.loraStack || job.library?.loraStack || params.loraName || job.library?.loraName
|
||||
loraStack: params.loraStack || job.library?.loraStack || params.loraName || job.library?.loraName,
|
||||
saveLosslessAnchor: params.saveLosslessAnchor === true || job.library?.saveLosslessAnchor === true
|
||||
})
|
||||
|
||||
const queued = await queuePrompt(graph, job.clientId)
|
||||
|
||||
Reference in New Issue
Block a user