Show small stitch-frame thumbs when a video collection is expanded.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-28 07:14:34 -05:00
co-authored by Cursor
parent 91948dd329
commit 010ef6add2
6 changed files with 264 additions and 21 deletions
+18
View File
@@ -89,6 +89,24 @@ export async function extractLastFrame(inputPath: string, outputPath: string) {
throw new Error('Could not extract the last frame. The video looks incomplete — interrupting Comfy mid-encode usually causes this. Earlier saved parts are still in the library.')
}
export async function extractFirstFrame(inputPath: string, outputPath: string) {
if (!existsSync(inputPath)) {
throw new Error('Source video is missing')
}
await runFfmpeg([
'-y',
'-i', inputPath,
'-frames:v', '1',
'-update', '1',
'-an',
'-q:v', '4',
outputPath
], 20000)
if (!existsSync(outputPath)) {
throw new Error('Could not extract the first frame')
}
}
export async function probeHasAudio(filePath: string) {
try {
const out = await runFfprobe([
+45
View File
@@ -5,6 +5,7 @@ import { spawn } from 'node:child_process'
import { randomBytes, scrypt, timingSafeEqual } from 'node:crypto'
import { promisify } from 'node:util'
import type { H3Event } from 'h3'
import { extractFirstFrame, extractLastFrame } from '~/server/utils/ffmpeg'
const scryptAsync = promisify(scrypt)
@@ -576,6 +577,50 @@ export function clipLastFramePath(owner: string, id: string) {
return join(clipDir(owner, id), 'last-frame.png')
}
export function clipFirstFramePath(owner: string, id: string) {
return join(clipDir(owner, id), 'first-frame.jpg')
}
export function clipFrameThumbFile(owner: string, id: string, kind: 'first' | 'last') {
return join(clipDir(owner, id), kind === 'last' ? 'last-frame.thumb.jpg' : 'first-frame.thumb.jpg')
}
export async function ensureScaledThumb(source: string, dest: string) {
if (!existsSync(source)) return ''
if (existsSync(dest)) {
try {
if (statSync(dest).mtimeMs >= statSync(source).mtimeMs) return dest
} catch { /* rebuild */ }
}
const key = dest
const pending = thumbBuilds.get(key)
if (pending) return pending
const build = withThumbSlot(async () => {
const ok = await buildStillThumb(source, dest)
return ok ? dest : source
}).finally(() => {
thumbBuilds.delete(key)
})
thumbBuilds.set(key, build)
return build
}
export async function ensureClipLastFrame(owner: string, id: string) {
const video = clipVideoPath(owner, id)
const dest = clipLastFramePath(owner, id)
if (!existsSync(video)) return ''
if (!existsSync(dest)) await extractLastFrame(video, dest)
return existsSync(dest) ? dest : ''
}
export async function ensureClipFirstFrame(owner: string, id: string) {
const video = clipVideoPath(owner, id)
const dest = clipFirstFramePath(owner, id)
if (!existsSync(video)) return ''
if (!existsSync(dest)) await extractFirstFrame(video, dest)
return existsSync(dest) ? dest : ''
}
export function extendTempDir(owner: string, jobId: string) {
const dir = join(ownerRoot(owner), 'tmp', 'extend', jobId)
mkdirSync(dir, { recursive: true })