Group extension chains in the library and add shot-script queues.

Keep every saved segment, show the longest clip with an expand list, branch on extend, and parse shot 2/3 blocks into a shared-duration queue with live part status.
This commit is contained in:
2026-08-26 08:03:41 -05:00
parent d78c3fee7d
commit db8c9aabba
9 changed files with 375 additions and 89 deletions
+1 -1
View File
@@ -101,7 +101,7 @@ function parseRate(rate: string) {
return Number.isFinite(value) && value > 0 ? value : NaN
}
async function probeDuration(filePath: string) {
export async function probeDuration(filePath: string) {
const raw = await runFfprobe([
'-v', 'error',
'-select_streams', 'v:0',
+2
View File
@@ -73,6 +73,8 @@ export interface Job {
extendTmpDir?: string
extendPart1Path?: string
extendSourceClipId?: string
familyId?: string
parentClipId?: string
}
error?: string
socketReady?: boolean
+23 -2
View File
@@ -34,6 +34,10 @@ export interface LibraryClip {
fps?: number
samplerName?: string
scheduler?: string
duration?: number
familyId?: string
parentClipId?: string
chainIndex?: number
}
export interface LibraryStill {
@@ -800,6 +804,10 @@ export async function saveClip(params: {
fps?: number
samplerName?: string
scheduler?: string
duration?: number
familyId?: string
parentClipId?: string
chainIndex?: number
}) {
const catalog = readCatalog(params.ownerKey)
const folder = catalog.folders.find(item => item.id === params.folderId) || catalog.folders[0]
@@ -821,10 +829,23 @@ export async function saveClip(params: {
cfg: params.cfg,
fps: params.fps,
samplerName: params.samplerName,
scheduler: params.scheduler
scheduler: params.scheduler,
familyId: params.familyId,
parentClipId: params.parentClipId,
chainIndex: params.chainIndex
}
mkdirSync(clipDir(params.ownerKey, clip.id), { recursive: true })
writeFileSync(clipVideoPath(params.ownerKey, clip.id), params.video)
const videoPath = clipVideoPath(params.ownerKey, clip.id)
writeFileSync(videoPath, params.video)
if (typeof params.duration === 'number' && params.duration > 0) {
clip.duration = params.duration
} else {
try {
clip.duration = await probeDuration(videoPath)
} catch {
/* duration stays unset */
}
}
if (params.thumb?.length) {
writeFileSync(join(clipDir(params.ownerKey, clip.id), 'thumb'), params.thumb)
} else {
+7 -1
View File
@@ -22,6 +22,9 @@ export interface PendingJob {
imageSubfolder?: string
extendTmpDir?: string
extendPart1Path?: string
familyId?: string
parentClipId?: string
chainIndex?: number
}
function pendingRoot() {
@@ -102,7 +105,10 @@ export async function completePendingIfReady(pending: PendingJob) {
hideThumbnail: pending.hideThumbnail,
video: buffer,
thumb: null,
comfyFilename: video.filename
comfyFilename: video.filename,
familyId: pending.familyId,
parentClipId: pending.parentClipId,
chainIndex: pending.chainIndex
})
await purgeComfyArtifacts({ video, imageName: pending.imageName, imageSubfolder: pending.imageSubfolder, promptId: pending.promptId })
deletePendingJob(pending.jobId)
+5 -1
View File
@@ -178,12 +178,16 @@ export function watchComfyJob(job: Job, options: { persist?: boolean } = {}): Pr
cfg: job.library.cfg,
fps: job.library.fps,
samplerName: job.library.samplerName,
scheduler: job.library.scheduler
scheduler: job.library.scheduler,
familyId: job.library.familyId || (job.library.familyId = crypto.randomUUID()),
parentClipId: job.library.parentClipId,
chainIndex: job.library.chainIndex
})
job.clipId = clip.id
job.hideThumbnail = clip.hideThumbnail
job.library.thumb = undefined
job.library.name = nextClipPartName(clip.name)
job.library.parentClipId = clip.id
await purgeComfyArtifacts({
video,
imageName: job.library.imageName,