Ship extension queues, library extend, and smoother seam stitching.

Users can pre-queue extensions, extend from the library with p2 titles, blend seams with xfade, and a unreadable catalog is no longer replaced with an empty one.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-26 00:01:25 -05:00
co-authored by Cursor
parent 22d99f18db
commit f5c709924b
7 changed files with 653 additions and 79 deletions
+25 -3
View File
@@ -57,6 +57,11 @@ export interface PublicFolder {
clipCount: number
}
export interface QueuedExtension {
prompt: string
duration: number
}
export interface RetryDraft {
id: string
createdAt: number
@@ -79,6 +84,7 @@ export interface RetryDraft {
fps?: number
samplerName?: string
scheduler?: string
extensions?: QueuedExtension[]
}
interface Catalog {
@@ -198,11 +204,15 @@ function ensureOwner(owner: string) {
function readCatalog(owner: string): Catalog {
ensureOwner(owner)
const path = catalogPath(owner)
try {
return normalizeCatalog(JSON.parse(readFileSync(catalogPath(owner), 'utf8')))
return normalizeCatalog(JSON.parse(readFileSync(path, 'utf8')))
} catch {
if (existsSync(path)) {
throw createError({ statusCode: 500, statusMessage: 'Library catalog could not be read' })
}
const created = emptyCatalog()
writeFileSync(catalogPath(owner), JSON.stringify(created, null, 2))
writeFileSync(path, JSON.stringify(created, null, 2))
return created
}
}
@@ -233,6 +243,16 @@ export function clipTitle(clip: Pick<LibraryClip, 'name' | 'prompt'>) {
return fromPrompt || 'Untitled clip'
}
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 safeDownloadName(name: string) {
const cleaned = name.replace(/[<>:"/\\|?*'`\u0000-\u001f]/g, '-').replace(/\s+/g, ' ').trim()
return cleaned.slice(0, 80) || 'aigen'
@@ -689,6 +709,7 @@ export async function saveRetryDraft(params: {
fps?: number
samplerName?: string
scheduler?: string
extensions?: QueuedExtension[]
}) {
return mutate(params.ownerKey, (catalog) => {
const existing = params.id ? catalog.drafts.find(item => item.id === params.id) : undefined
@@ -713,7 +734,8 @@ export async function saveRetryDraft(params: {
cfg: params.cfg,
fps: params.fps,
samplerName: params.samplerName,
scheduler: params.scheduler
scheduler: params.scheduler,
extensions: params.extensions?.length ? params.extensions : undefined
}
catalog.drafts = [draft, ...catalog.drafts.filter(item => item.id !== draft.id)].slice(0, 20)
return draft