Save each Comfy run as a new clip even if the output filename repeats.

Purging desktop files let MiniMax reuse MiniMax_H3_00001_, so the next job returned the previous clip. Ignore stale events from the last job so a second generate cannot paste the first video over the new one.
This commit is contained in:
2026-08-25 20:29:36 -05:00
parent c5ed4d7ac4
commit ba7a483aac
6 changed files with 36 additions and 15 deletions
+26 -10
View File
@@ -487,6 +487,7 @@ let timer: ReturnType<typeof setInterval> | null = null
let source: EventSource | null = null let source: EventSource | null = null
let pollTimer: ReturnType<typeof setInterval> | null = null let pollTimer: ReturnType<typeof setInterval> | null = null
let settledUi = false let settledUi = false
let listenGen = 0
const elapsedLabel = computed(() => { const elapsedLabel = computed(() => {
const seconds = Math.floor(elapsedMs.value / 1000) const seconds = Math.floor(elapsedMs.value / 1000)
@@ -739,7 +740,7 @@ async function resumeActiveJob() {
jobId.value = id jobId.value = id
concealOutput.value = hidden concealOutput.value = hidden
settledUi = false settledUi = false
applyJobEvent(snap, hidden, locked) applyJobEvent(snap, hidden, locked, id)
const running = snap.status === 'queued' || snap.status === 'uploading' || snap.status === 'running' || snap.type === 'snapshot' const running = snap.status === 'queued' || snap.status === 'uploading' || snap.status === 'running' || snap.type === 'snapshot'
if (running && !settledUi) { if (running && !settledUi) {
busy.value = true busy.value = true
@@ -828,10 +829,14 @@ async function lockFolder(id: string) {
} }
} }
function clipVideoUrl(clipId: string) {
return `/api/library/clips/${clipId}/video?t=${Date.now()}`
}
function openClip(item: LibraryClip) { function openClip(item: LibraryClip) {
lockedSave.value = false lockedSave.value = false
concealOutput.value = item.hideThumbnail concealOutput.value = item.hideThumbnail
videoUrl.value = `/api/library/clips/${item.id}/video` videoUrl.value = clipVideoUrl(item.id)
awaitingReveal.value = item.hideThumbnail awaitingReveal.value = item.hideThumbnail
playerSize.value = { width: item.width || 1344, height: item.height || 768 } playerSize.value = { width: item.width || 1344, height: item.height || 768 }
browseFolderId.value = item.folderId browseFolderId.value = item.folderId
@@ -874,6 +879,8 @@ async function generate() {
} }
statusBusy.value = false statusBusy.value = false
busy.value = true busy.value = true
listenGen += 1
stopListen()
settledUi = false settledUi = false
progress.value = 1 progress.value = 1
videoUrl.value = '' videoUrl.value = ''
@@ -905,6 +912,7 @@ async function generate() {
jobId.value = started.jobId jobId.value = started.jobId
maxStep.value = started.steps maxStep.value = started.steps
playerSize.value = { width: started.width, height: started.height } playerSize.value = { width: started.width, height: started.height }
settledUi = false
persistActiveJob(started.jobId, started.hideThumbnail === true || hideThumbnail.value, started.folderLocked === true) persistActiveJob(started.jobId, started.hideThumbnail === true || hideThumbnail.value, started.folderLocked === true)
listen(started.jobId, started.hideThumbnail === true || hideThumbnail.value, started.folderLocked === true) listen(started.jobId, started.hideThumbnail === true || hideThumbnail.value, started.folderLocked === true)
} catch (error: any) { } catch (error: any) {
@@ -927,7 +935,10 @@ function stopListen() {
} }
} }
function applyJobEvent(payload: Record<string, any>, hidden: boolean, folderLocked: boolean) { function applyJobEvent(payload: Record<string, any>, hidden: boolean, folderLocked: boolean, expectedJobId: string) {
if (expectedJobId && jobId.value && expectedJobId !== jobId.value) return
if (payload.jobId && expectedJobId && payload.jobId !== expectedJobId) return
if (payload.message) statusMessage.value = payload.message if (payload.message) statusMessage.value = payload.message
statusBusy.value = payload.type === 'busy' || payload.busy === true statusBusy.value = payload.type === 'busy' || payload.busy === true
if (typeof payload.progress === 'number') progress.value = payload.progress if (typeof payload.progress === 'number') progress.value = payload.progress
@@ -952,12 +963,12 @@ function applyJobEvent(payload: Record<string, any>, hidden: boolean, folderLock
browseFolderId.value = folderId.value browseFolderId.value = folderId.value
statusMessage.value = 'Saved to the locked folder. Unlock it to view.' statusMessage.value = 'Saved to the locked folder. Unlock it to view.'
} else if (payload.clipId) { } else if (payload.clipId) {
videoUrl.value = `/api/library/clips/${payload.clipId}/video` videoUrl.value = clipVideoUrl(payload.clipId)
awaitingReveal.value = Boolean(payload.hideThumbnail || hidden) awaitingReveal.value = Boolean(payload.hideThumbnail || hidden)
concealOutput.value = awaitingReveal.value concealOutput.value = awaitingReveal.value
lockedSave.value = false lockedSave.value = false
} else { } else {
videoUrl.value = `/api/media?filename=${encodeURIComponent(payload.filename)}&subfolder=${encodeURIComponent(payload.subfolder || 'video')}&type=${encodeURIComponent(payload.mediaType || 'output')}` videoUrl.value = `/api/media?filename=${encodeURIComponent(payload.filename)}&subfolder=${encodeURIComponent(payload.subfolder || 'video')}&type=${encodeURIComponent(payload.mediaType || 'output')}&t=${Date.now()}`
awaitingReveal.value = Boolean(payload.hideThumbnail || hidden) awaitingReveal.value = Boolean(payload.hideThumbnail || hidden)
concealOutput.value = awaitingReveal.value concealOutput.value = awaitingReveal.value
lockedSave.value = false lockedSave.value = false
@@ -983,26 +994,31 @@ function applyJobEvent(payload: Record<string, any>, hidden: boolean, folderLock
} }
function listen(id: string, hidden: boolean, folderLocked = false) { function listen(id: string, hidden: boolean, folderLocked = false) {
const gen = ++listenGen
stopListen() stopListen()
source = new EventSource(`/api/generate/${id}/stream`) source = new EventSource(`/api/generate/${id}/stream`)
source.onmessage = (event) => { source.onmessage = (event) => {
applyJobEvent(JSON.parse(event.data), hidden, folderLocked) if (gen !== listenGen) return
applyJobEvent(JSON.parse(event.data), hidden, folderLocked, id)
} }
const tick = async () => { const tick = async () => {
if (settledUi) { if (gen !== listenGen || settledUi) {
stopListen() if (gen === listenGen && settledUi) stopListen()
return return
} }
try { try {
const snap = await $fetch<Record<string, any>>(`/api/generate/${id}`) const snap = await $fetch<Record<string, any>>(`/api/generate/${id}`)
applyJobEvent(snap, hidden, folderLocked) if (gen !== listenGen) return
applyJobEvent(snap, hidden, folderLocked, id)
} catch { } catch {
if (gen !== listenGen) return
try { try {
const recovered = await $fetch<Record<string, any>>('/api/generate/recover', { const recovered = await $fetch<Record<string, any>>('/api/generate/recover', {
method: 'POST', method: 'POST',
body: { jobId: id } body: { jobId: id }
}) })
applyJobEvent(recovered, hidden, folderLocked) if (gen !== listenGen) return
applyJobEvent(recovered, hidden, folderLocked, id)
} catch { } catch {
// Job snapshot can 404 briefly after a restart; keep polling while busy. // Job snapshot can 404 briefly after a restart; keep polling while busy.
} }
+5 -1
View File
@@ -12,12 +12,16 @@ export default defineEventHandler(async (event) => {
return { return {
type: 'snapshot', type: 'snapshot',
status: 'running', status: 'running',
jobId,
message: 'Reconnecting to ComfyUI...', message: 'Reconnecting to ComfyUI...',
progress: 90, progress: 90,
promptId: pending.promptId, promptId: pending.promptId,
elapsedMs: Date.now() - pending.startedAt elapsedMs: Date.now() - pending.startedAt,
hideThumbnail: pending.hideThumbnail,
folderLocked: pending.folderLocked
} }
} }
throw createError({ statusCode: 404, statusMessage: 'Job not found' })
} }
const { owner } = assertLibraryOwner(event) const { owner } = assertLibraryOwner(event)
+1 -1
View File
@@ -11,6 +11,6 @@ export default defineEventHandler((event) => {
} }
setHeader(event, 'Content-Type', 'video/mp4') setHeader(event, 'Content-Type', 'video/mp4')
setHeader(event, 'Content-Disposition', `inline; filename="${safeDownloadName(clipTitle(clip))}.mp4"`) setHeader(event, 'Content-Disposition', `inline; filename="${safeDownloadName(clipTitle(clip))}.mp4"`)
setHeader(event, 'Cache-Control', 'private, max-age=3600') setHeader(event, 'Cache-Control', 'private, no-store')
return readFileSync(path) return readFileSync(path)
}) })
+3
View File
@@ -18,6 +18,7 @@ export interface JobEvent {
queueRunning?: number queueRunning?: number
queuePending?: number queuePending?: number
folderLocked?: boolean folderLocked?: boolean
jobId?: string
} }
export interface Job { export interface Job {
@@ -89,6 +90,7 @@ export function listJobs() {
export function jobSnapshot(job: Job) { export function jobSnapshot(job: Job) {
return { return {
jobId: job.id,
type: 'snapshot' as const, type: 'snapshot' as const,
status: job.status, status: job.status,
message: job.message, message: job.message,
@@ -108,6 +110,7 @@ export function jobSnapshot(job: Job) {
} }
export function emitJob(job: Job, event: JobEvent) { export function emitJob(job: Job, event: JobEvent) {
event.jobId = job.id
if (event.message) job.message = event.message if (event.message) job.message = event.message
if (typeof event.progress === 'number') job.progress = event.progress if (typeof event.progress === 'number') job.progress = event.progress
if (typeof event.step === 'number') job.step = event.step if (typeof event.step === 'number') job.step = event.step
-3
View File
@@ -637,9 +637,6 @@ export async function saveClip(params: {
const catalog = readCatalog(params.ownerKey) const catalog = readCatalog(params.ownerKey)
const folder = catalog.folders.find(item => item.id === params.folderId) || catalog.folders[0] const folder = catalog.folders.find(item => item.id === params.folderId) || catalog.folders[0]
if (!folder) throw createError({ statusCode: 400, statusMessage: 'No library folder available' }) if (!folder) throw createError({ statusCode: 400, statusMessage: 'No library folder available' })
if (params.comfyFilename && catalog.clips.some(clip => clip.comfyFilename === params.comfyFilename)) {
return catalog.clips.find(clip => clip.comfyFilename === params.comfyFilename) as LibraryClip
}
const clip: LibraryClip = { const clip: LibraryClip = {
id: crypto.randomUUID(), id: crypto.randomUUID(),
folderId: folder.id, folderId: folder.id,
+1
View File
@@ -91,6 +91,7 @@ export async function completePendingIfReady(pending: PendingJob) {
status: 'complete' as const, status: 'complete' as const,
message: pending.folderLocked ? 'Saved to the locked folder. Unlock it to view.' : 'Video ready', message: pending.folderLocked ? 'Saved to the locked folder. Unlock it to view.' : 'Video ready',
progress: 100, progress: 100,
jobId: pending.jobId,
clipId: clip.id, clipId: clip.id,
filename: pending.folderLocked ? undefined : video.filename, filename: pending.folderLocked ? undefined : video.filename,
subfolder: pending.folderLocked ? undefined : video.subfolder, subfolder: pending.folderLocked ? undefined : video.subfolder,