Keep the Extend panel put and name sibling clips as branches.
A finished job no longer closes a draft or jumps the page, and a second extend from the same part saves as p8b instead of another p8. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { readLoraStack } from '~/utils/loras'
|
||||
import { parseExtendDuration } from '~/server/utils/extendChain'
|
||||
import { defaultVideoSteps, isLtxWorkflow, LTX_DISABLED_MESSAGE, ltxWorkflowEnabled, parseVideoWorkflow } from '~/utils/videoModels'
|
||||
import { composePromptParts } from '~/utils/promptParts'
|
||||
import { nextFamilyPartName } from '~/server/utils/library'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody<{ clipId?: string; prompt?: string; promptPre?: string; promptPost?: string; duration?: number; lora?: unknown; loraStack?: unknown }>(event).catch(() => ({}))
|
||||
@@ -45,6 +46,10 @@ export default defineEventHandler(async (event) => {
|
||||
const turbo = source.turbo !== false
|
||||
const steps = Number(source.steps || defaultVideoSteps(turbo, workflow))
|
||||
const seed = Math.floor(Math.random() * 2_147_483_647)
|
||||
const reservedNames = listStudioJobs(owner)
|
||||
.filter(job => job.status === 'waiting' || job.status === 'running' || job.status === 'held')
|
||||
.map(job => job.name)
|
||||
.filter(Boolean)
|
||||
|
||||
const studio = await addStudioJob({
|
||||
ownerKey: owner,
|
||||
@@ -54,7 +59,7 @@ export default defineEventHandler(async (event) => {
|
||||
promptMid: prompt,
|
||||
promptPre,
|
||||
promptPost,
|
||||
name: nextClipPartName(clipTitle(source)),
|
||||
name: nextFamilyPartName(owner, source, reservedNames),
|
||||
folderId: source.folderId,
|
||||
aspect: source.aspect || '16:9',
|
||||
width: source.width || 1344,
|
||||
@@ -95,6 +100,7 @@ export default defineEventHandler(async (event) => {
|
||||
queued: !liveJobId,
|
||||
clientId: '',
|
||||
seed,
|
||||
name: studio.name,
|
||||
width: source.width || 1344,
|
||||
height: source.height || 768,
|
||||
steps,
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
clipVideoPath,
|
||||
extendTempDir,
|
||||
getClip,
|
||||
nextClipPartName,
|
||||
nextFamilyPartName,
|
||||
removeExtendTemp
|
||||
} from '~/server/utils/library'
|
||||
import { extractLastFrame, probeHasAudio } from '~/server/utils/ffmpeg'
|
||||
@@ -63,6 +63,7 @@ export async function beginExtendFromClip(params: {
|
||||
duration?: number
|
||||
loraStack?: unknown
|
||||
folderLocked?: boolean
|
||||
name?: string
|
||||
}) {
|
||||
const source = getClip(params.ownerKey, params.clipId)
|
||||
const sourcePath = clipVideoPath(params.ownerKey, source.id)
|
||||
@@ -104,7 +105,7 @@ export async function beginExtendFromClip(params: {
|
||||
const length = frameLength(durationSeconds, fps)
|
||||
const width = source.width || 1344
|
||||
const height = source.height || 768
|
||||
const clipName = nextClipPartName(clipTitle(source))
|
||||
const clipName = String(params.name || '').trim() || nextFamilyPartName(params.ownerKey, source)
|
||||
|
||||
const job = createJob()
|
||||
job.kind = 'video'
|
||||
|
||||
+7
-12
@@ -18,6 +18,9 @@ import {
|
||||
type KeepPromptKind,
|
||||
type KeepPromptLists
|
||||
} from '~/utils/keepPrompts'
|
||||
import { familyBaseName, nextClipPartName, clipPartLabel } from '~/utils/libraryBrowse'
|
||||
|
||||
export { familyBaseName, nextClipPartName, clipPartLabel }
|
||||
|
||||
const scryptAsync = promisify(scrypt)
|
||||
|
||||
@@ -327,18 +330,10 @@ export function clipTitle(clip: Pick<LibraryClip, 'name' | 'prompt'>) {
|
||||
return fromPrompt || 'Untitled clip'
|
||||
}
|
||||
|
||||
export function familyBaseName(name: string) {
|
||||
return String(name || '').trim().replace(/\s+p\d+$/i, '')
|
||||
}
|
||||
|
||||
export function nextClipPartName(name: string) {
|
||||
const trimmed = (name || '').trim()
|
||||
if (!trimmed) return ''
|
||||
const match = trimmed.match(/^(.*)\s+p(\d+)$/i)
|
||||
const base = (match ? match[1] : trimmed).trim() || trimmed
|
||||
const part = match ? Number(match[2]) + 1 : 2
|
||||
const suffix = ` p${part}`
|
||||
return `${base.slice(0, Math.max(1, 80 - suffix.length))}${suffix}`
|
||||
export function nextFamilyPartName(owner: string, source: LibraryClip, extraNames: string[] = []) {
|
||||
const catalog = readCatalog(owner)
|
||||
const family = clipsInSameFamily(catalog, source)
|
||||
return nextClipPartName(clipTitle(source), [...family.map(item => clipTitle(item)), ...extraNames])
|
||||
}
|
||||
|
||||
export function sortFamilyParts(a: LibraryClip, b: LibraryClip) {
|
||||
|
||||
@@ -951,7 +951,8 @@ async function startStudioExtendJob(item: StudioJob) {
|
||||
promptPost: item.payload.promptPost,
|
||||
duration: item.payload.duration,
|
||||
loraStack: item.payload.loraStack || item.payload.loraName,
|
||||
folderLocked: item.payload.folderLocked
|
||||
folderLocked: item.payload.folderLocked,
|
||||
name: item.payload.name || item.name
|
||||
})
|
||||
await markStudioLive(item.ownerKey, item.id, live.id)
|
||||
} catch (error) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { emitChainJob, waitForComfySocket, watchComfyJob } from '~/server/utils/
|
||||
import { comfyFilenamePrefix, queuePrompt, uploadImage } from '~/server/utils/comfy'
|
||||
import { buildWorkflow } from '~/server/utils/workflow'
|
||||
import { isLtxWorkflow, isTextToVideo, parseVideoWorkflow, workflowForExtension, type VideoWorkflowId } from '~/utils/videoModels'
|
||||
import { clipVideoPath, clipTitle, deleteRetryDraft, extendTempDir, getClip, nextClipPartName, removeExtendTemp, stillPath } from '~/server/utils/library'
|
||||
import { clipVideoPath, clipTitle, deleteRetryDraft, extendTempDir, getClip, nextFamilyPartName, removeExtendTemp, stillPath } from '~/server/utils/library'
|
||||
import { extractLastFrame, probeHasAudio } from '~/server/utils/ffmpeg'
|
||||
import { ensureComfyReady } from '~/server/utils/comfyLifecycle'
|
||||
import { onLiveVideoSettled, onStudioQueueBurstStarted } from '~/server/utils/studioQueue'
|
||||
@@ -574,7 +574,7 @@ export async function startQueueBurst(owner: string, queueId: string, count: num
|
||||
hideThumbnail: queue.hideThumbnail,
|
||||
hideInput: queue.hideInput,
|
||||
folderLocked: destFolderLocked,
|
||||
name: nextClipPartName(clipTitle(source)),
|
||||
name: nextFamilyPartName(owner, source),
|
||||
prompt: queue.segments[lastIndex]?.prompt || source.prompt,
|
||||
aspect: queue.aspect,
|
||||
width: queue.width,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { syncQueueFromJob } from '~/server/utils/shotQueue'
|
||||
import { isEncodingNode, NODE_LABELS } from '~/server/utils/workflow'
|
||||
import { extractFirstFrameFromBuffer } from '~/server/utils/ffmpeg'
|
||||
import { mergePermanenceRefs, type PermanenceRef } from '~/utils/globalLocks'
|
||||
import { nextFamilyPartName } from '~/server/utils/library'
|
||||
|
||||
function clipRefsFromJob(library: NonNullable<Job['library']>): PermanenceRef[] | undefined {
|
||||
const refs = mergePermanenceRefs(library.permanenceRefs, library.shotPermanenceRefs?.[library.chainIndex || 0])
|
||||
@@ -252,7 +253,7 @@ export function watchComfyJob(job: Job, options: { persist?: boolean } = {}): Pr
|
||||
job.savedPromptId = job.promptId
|
||||
job.hideThumbnail = clip.hideThumbnail
|
||||
job.library.thumb = undefined
|
||||
job.library.name = nextClipPartName(clip.name)
|
||||
job.library.name = nextFamilyPartName(job.library.ownerKey, clip)
|
||||
job.library.parentClipId = clip.id
|
||||
await syncQueueFromJob(job, clip.id).catch(() => null)
|
||||
await purgeComfyArtifacts({
|
||||
|
||||
Reference in New Issue
Block a user