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
+89 -1
View File
@@ -1109,6 +1109,41 @@
<span class="mt-0.5 block text-xs text-zinc-500">Overnight set-and-forget. Leave this off to edit prompts on Queue (under this job) and process the next shot when you are ready.</span>
</span>
</label>
<div v-if="studioMode === 'video'" class="space-y-3">
<label
class="flex cursor-pointer items-start gap-3 rounded-2xl border border-white/10 bg-zinc-950/40 px-4 py-3"
title="Runs a low-denoise Bilateral + Flux cleanup pass on the handoff frame to strip compression artifacts."
>
<input v-model="refineExtensionFrame" type="checkbox" class="mt-1">
<span>
<span class="block text-sm font-medium text-zinc-200">Refine Extension Frame</span>
<span class="mt-0.5 block text-xs text-zinc-500">Runs a low-denoise Bilateral + Flux cleanup pass on the handoff frame to strip compression artifacts.</span>
</span>
</label>
<div
v-if="refineExtensionFrame"
class="rounded-2xl border border-white/10 bg-zinc-950/40 p-3"
>
<Thumbwheel
v-model="refinementDenoise"
label="Refinement Denoise"
:min="REFINEMENT_DENOISE_MIN"
:max="REFINEMENT_DENOISE_MAX"
:step="0.01"
:digits="2"
/>
</div>
<label
class="flex cursor-pointer items-start gap-3 rounded-2xl border border-white/10 bg-zinc-950/40 px-4 py-3"
title="Saves an uncompressed PNG of the final frame alongside the MP4 for cleaner future extensions."
>
<input v-model="saveLosslessAnchor" type="checkbox" class="mt-1">
<span>
<span class="block text-sm font-medium text-zinc-200">Save Lossless Anchor Frame</span>
<span class="mt-0.5 block text-xs text-zinc-500">Saves an uncompressed PNG of the final frame alongside the MP4 for cleaner future extensions.</span>
</span>
</label>
</div>
<div class="flex flex-wrap gap-3">
<button
type="button"
@@ -1458,6 +1493,38 @@
@insert-trigger="insertLoraTrigger($event, 'extend')"
@update-trigger="saveLoraTrigger"
/>
<div class="space-y-3">
<label
class="flex cursor-pointer items-start gap-3 rounded-xl border border-white/10 bg-zinc-950/40 px-3 py-2.5"
title="Runs a low-denoise Bilateral + Flux cleanup pass on the handoff frame to strip compression artifacts."
>
<input v-model="refineExtensionFrame" type="checkbox" class="mt-1">
<span>
<span class="block text-sm font-medium text-zinc-200">Refine Extension Frame</span>
<span class="mt-0.5 block text-xs text-zinc-500">Bilateral + Flux cleanup on the handoff frame.</span>
</span>
</label>
<div v-if="refineExtensionFrame" class="rounded-xl border border-white/10 bg-zinc-950/40 p-3">
<Thumbwheel
v-model="refinementDenoise"
label="Refinement Denoise"
:min="REFINEMENT_DENOISE_MIN"
:max="REFINEMENT_DENOISE_MAX"
:step="0.01"
:digits="2"
/>
</div>
<label
class="flex cursor-pointer items-start gap-3 rounded-xl border border-white/10 bg-zinc-950/40 px-3 py-2.5"
title="Saves an uncompressed PNG of the final frame alongside the MP4 for cleaner future extensions."
>
<input v-model="saveLosslessAnchor" type="checkbox" class="mt-1">
<span>
<span class="block text-sm font-medium text-zinc-200">Save Lossless Anchor Frame</span>
<span class="mt-0.5 block text-xs text-zinc-500">Keep a PNG anchor with this extension for cleaner next hops.</span>
</span>
</label>
</div>
<button
type="button"
class="w-full rounded-2xl bg-amber-400 px-4 py-2.5 text-sm font-semibold text-zinc-950 transition hover:bg-amber-300 disabled:cursor-not-allowed disabled:opacity-50"
@@ -2324,6 +2391,12 @@ import {
defaultVideoDurationPref,
setDefaultVideoDurationPref
} from '~/utils/videoDuration'
import {
clampRefinementDenoise,
REFINEMENT_DENOISE_DEFAULT,
REFINEMENT_DENOISE_MAX,
REFINEMENT_DENOISE_MIN
} from '~/utils/extensionFrame'
const runtimeConfig = useRuntimeConfig()
const route = useRoute()
@@ -2758,6 +2831,9 @@ const renameDraft = ref('')
const renameInput = ref<HTMLInputElement | null>(null)
const unlockPassword = ref('')
const queueAutoRun = ref(false)
const refineExtensionFrame = ref(true)
const saveLosslessAnchor = ref(false)
const refinementDenoise = ref(REFINEMENT_DENOISE_DEFAULT)
const queuePendingCount = ref(0)
const activeQueueId = ref('')
const liveQueue = ref<{
@@ -5452,6 +5528,9 @@ async function restoreStudioPayload(kind: string, payload: {
referenceStillIds?: Array<string | null>
extensions?: { prompt: string; promptPre?: string; promptPost?: string; duration: number; permanenceRefs?: PermanenceRef[]; loraName?: string; loraStack?: LoraStackItem[] }[]
queueAutoRun?: boolean
refineExtensionFrame?: boolean
saveLosslessAnchor?: boolean
refinementDenoise?: number
promptPre?: string
promptPost?: string
globalLocks?: string
@@ -5520,6 +5599,9 @@ async function restoreStudioPayload(kind: string, payload: {
readLoraStack(item)
))
queueAutoRun.value = payload.queueAutoRun === true
refineExtensionFrame.value = payload.refineExtensionFrame !== false
saveLosslessAnchor.value = payload.saveLosslessAnchor === true
refinementDenoise.value = clampRefinementDenoise(payload.refinementDenoise, REFINEMENT_DENOISE_DEFAULT)
globalLocks.value = payload.globalLocks || ''
familyPermanenceRefs.value = payload.permanenceRefs || []
const shotRefs: Record<number, PermanenceRef[]> = {}
@@ -6113,7 +6195,10 @@ async function generateExtension() {
promptPre: extendPromptPre.value,
promptPost: extendPromptPost.value,
duration: extendDuration.value,
loraStack: extendLoraStack.value
loraStack: extendLoraStack.value,
refineExtensionFrame: refineExtensionFrame.value,
saveLosslessAnchor: saveLosslessAnchor.value,
refinementDenoise: clampRefinementDenoise(refinementDenoise.value)
}
})
if (queuedOffLiveOutput(started, 'video')) {
@@ -7391,6 +7476,9 @@ async function generate() {
}))))
body.append('queueAutoRun', String(queueAutoRun.value))
}
body.append('refineExtensionFrame', String(refineExtensionFrame.value))
body.append('saveLosslessAnchor', String(saveLosslessAnchor.value))
body.append('refinementDenoise', String(clampRefinementDenoise(refinementDenoise.value)))
const downloadName = clipFileName(clipName.value.trim() || autoOutputName.value)
const chainPlan = plannedChain.value.map(part => ({ ...part }))
console.info('[aigen generate payload]', {