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
+65 -6
View File
@@ -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)