From 110865b370462b6df3b5aa132c5c6f2c1ff8f6f3 Mon Sep 17 00:00:00 2001 From: Towsty Date: Thu, 3 Sep 2026 08:30:05 -0500 Subject: [PATCH] Delete Comfy video, image, and audio files from the desktop after they are saved to the library. Audio never called purge, image stills used untagged names, and a locked file could abort the whole host-agent delete. Co-authored-by: Cursor --- scripts/comfy-host-agent.mjs | 149 ++++++++++++++++++++++++++++++----- server/utils/comfy.ts | 74 +++++++++++++---- server/utils/imageChain.ts | 4 +- server/utils/imageChainV2.ts | 4 +- server/utils/imageComfy.ts | 14 +++- server/utils/library.ts | 1 + server/utils/musicChain.ts | 6 +- 7 files changed, 209 insertions(+), 43 deletions(-) diff --git a/scripts/comfy-host-agent.mjs b/scripts/comfy-host-agent.mjs index 235b1d2..1f8b97a 100644 --- a/scripts/comfy-host-agent.mjs +++ b/scripts/comfy-host-agent.mjs @@ -2,7 +2,7 @@ import http from 'node:http' import net from 'node:net' import { execFile, spawn } from 'node:child_process' import { promisify } from 'node:util' -import { readdirSync, existsSync, rmSync, readFileSync, openSync, mkdirSync, writeFileSync, unlinkSync } from 'node:fs' +import { readdirSync, existsSync, rmSync, readFileSync, openSync, mkdirSync, writeFileSync, unlinkSync, statSync } from 'node:fs' import { basename, dirname, join, resolve, relative, isAbsolute } from 'node:path' import { tmpdir } from 'node:os' @@ -584,19 +584,6 @@ function rootsForType(type) { return [...outputRoots(), ...tempRoots()] } -function removeNamedFile(roots, filename, subfolders) { - const deleted = [] - const subs = [...new Set((subfolders || []).map(item => String(item || '')))] - for (const root of roots) { - if (!existsSync(root)) continue - for (const sub of subs) { - const path = safeFile(root, sub, filename) - if (removeFile(path)) deleted.push(path) - } - } - return deleted -} - function safeFile(root, subfolder, filename) { const name = basename(String(filename || '')) if (!name || name === '.' || name === '..') return null @@ -609,8 +596,91 @@ function safeFile(root, subfolder, filename) { function removeFile(path) { if (!path || !existsSync(path)) return false - rmSync(path, { force: true }) - return true + try { + rmSync(path, { force: true }) + return true + } catch { + return false + } +} + +function fileStem(filename) { + const name = basename(String(filename || '')) + const dot = name.lastIndexOf('.') + return dot > 0 ? name.slice(0, dot) : name +} + +function removeNamedFile(roots, filename, subfolders) { + const deleted = [] + const name = basename(String(filename || '')) + if (!name) return deleted + const stem = fileStem(name) + const subs = [...new Set((subfolders || []).map(item => String(item || '')))] + for (const root of roots) { + if (!existsSync(root)) continue + for (const sub of subs) { + const dir = sub ? join(root, String(sub).replace(/\.\./g, '')) : root + const exact = safeFile(root, sub, name) + if (removeFile(exact)) deleted.push(exact) + if (!stem || !existsSync(dir)) continue + let files = [] + try { files = readdirSync(dir) } catch { continue } + for (const file of files) { + if (file === name || file.startsWith(`${stem}.`)) { + const path = join(dir, file) + if (removeFile(path)) deleted.push(path) + } + } + } + } + return deleted +} + +function defaultSweepPrefixes() { + return ['MiniMax_H3', 'xAIGen', 'XAIgen', 'LTX23', 'AIGen', 'v2-krea', 'v2-generate'] +} + +function looksLikeStudioStill(name) { + return /^[0-9a-f]{12}(_p\d+)?(_\d{5})?_?\.(png|jpe?g|webp)$/i.test(String(name || '')) +} + +function isStudioOutputName(file, sub, prefixes) { + if (looksLikeStudioStill(file) && (!sub || sub === 'image' || sub === 'still')) return true + if (sub === 'audio') { + return prefixes.some(prefix => file.startsWith(prefix)) || file.startsWith('ComfyUI') + } + if (!sub) { + return prefixes.some(prefix => prefix !== 'LTX23' && file.startsWith(prefix)) + } + return prefixes.some(prefix => file.startsWith(prefix)) +} + +function sweepStaleStudioOutputs(prefixes, subfolders, maxAgeMs = 15 * 60 * 1000) { + const deleted = [] + const names = [...new Set([...defaultSweepPrefixes(), ...(prefixes || [])])].map(item => String(item || '').trim()).filter(Boolean) + const subs = [...new Set(['video', 'audio', 'image', 'still', ...(subfolders || [])])].map(item => String(item || '')) + const cutoff = Date.now() - Math.max(60_000, Number(maxAgeMs) || 900_000) + for (const root of outputRoots()) { + if (!existsSync(root)) continue + for (const sub of ['', ...subs]) { + const dir = sub ? join(root, sub) : root + if (!existsSync(dir)) continue + let files = [] + try { files = readdirSync(dir) } catch { continue } + for (const file of files) { + if (!isStudioOutputName(file, sub, names)) continue + const path = join(dir, file) + try { + const st = statSync(path) + if (st.isDirectory() || st.mtimeMs > cutoff) continue + } catch { + continue + } + if (removeFile(path)) deleted.push(path) + } + } + } + return deleted } function readJson(req) { @@ -649,25 +719,37 @@ function purgeDesktopFiles(body) { [video.subfolder, imageOut ? '' : 'video', ''] )) } + const audio = body?.audio || null + const audioName = String(audio?.filename || '') + if (audioName) { + deleted.push(...removeNamedFile( + rootsForType(String(audio?.type || 'output')), + audioName, + [audio.subfolder, 'audio', ''] + )) + } const output = body?.output || null const outputName = String(output?.filename || '') if (outputName) { deleted.push(...removeNamedFile( rootsForType(String(output?.type || 'output')), outputName, - [output.subfolder, ''] + [output.subfolder, 'image', 'still', ''] )) } + deleted.push(...sweepStaleStudioOutputs(body?.sweepPrefixes, body?.sweepSubfolders, body?.sweepMaxAgeMs)) + const unique = [...new Set(deleted)] console.log(JSON.stringify({ src: 'comfy-host-agent', event: 'purge', - deleted: deleted.length, - files: deleted.map((path) => basename(path)), + deleted: unique.length, + files: unique.map((path) => basename(path)), imageNames: names, video: videoName || null, + audio: audioName || null, output: outputName || null })) - return { ok: true, deleted } + return { ok: true, deleted: unique } } const server = http.createServer(async (req, res) => { @@ -760,14 +842,41 @@ const server = http.createServer(async (req, res) => { const body = await readJson(req) return json(res, 200, purgeDesktopFiles(body)) } + if (req.method === 'POST' && url.pathname === '/sweep') { + const body = await readJson(req) + const deleted = sweepStaleStudioOutputs(body?.sweepPrefixes, body?.sweepSubfolders, body?.sweepMaxAgeMs) + console.log(JSON.stringify({ + src: 'comfy-host-agent', + event: 'sweep', + deleted: deleted.length, + files: deleted.map((path) => basename(path)) + })) + return json(res, 200, { ok: true, deleted }) + } json(res, 404, { ok: false, error: 'not found' }) }) +function logStartupSweep() { + try { + const deleted = sweepStaleStudioOutputs() + if (!deleted.length) return + console.log(JSON.stringify({ + src: 'comfy-host-agent', + event: 'startup-sweep', + deleted: deleted.length, + files: deleted.map((path) => basename(path)) + })) + } catch (error) { + console.log(JSON.stringify({ src: 'comfy-host-agent', event: 'startup-sweep-error', error: String(error?.message || error) })) + } +} + server.listen(port, '0.0.0.0', async () => { ensureProxyListening() lastHealthyPort = await syncProxy() || 0 lastProcessUp = Boolean(lastHealthyPort) || await processUp().catch(() => false) if (lastHealthyPort) await noteQueue(lastHealthyPort) + logStartupSweep() console.log(JSON.stringify({ src: 'comfy-host-agent', event: 'listen', diff --git a/server/utils/comfy.ts b/server/utils/comfy.ts index d21e4a1..8ac135d 100644 --- a/server/utils/comfy.ts +++ b/server/utils/comfy.ts @@ -531,12 +531,59 @@ export function isOurComfyVideo(video: { filename: string; subfolder: string }) }) } +export function comfySweepNamePrefixes() { + const names = new Set(['MiniMax_H3', 'xAIGen', 'XAIgen', 'LTX23', 'AIGen', 'v2-krea', 'v2-generate']) + for (const raw of [comfyFilenamePrefix(), comfyAudioPrefix(), comfyImagePrefix()]) { + const last = raw.replace(/\/$/, '').split('/').pop() + if (last) names.add(last) + } + return [...names] +} + +export function comfyStudioFileTag() { + const last = comfyFilenamePrefix().replace(/\/$/, '').split('/').pop() || '' + if (/xaigen/i.test(last)) return 'xAIGen' + return 'AIGen' +} + +export function comfyImagePrefix() { + const config = useRuntimeConfig() + const explicit = String(config.comfyImagePrefix || process.env.COMFY_IMAGE_PREFIX || '').trim() + if (explicit) return explicit.replace(/\/$/, '') + return `image/${comfyStudioFileTag()}` +} + export function purgeComfyEnabled() { return useRuntimeConfig().purgeComfyOutputs !== false } +type ComfyDiskFile = { filename: string; subfolder?: string; type?: string } + +async function deleteComfyHttpFile(file: ComfyDiskFile) { + const payload = { + filename: file.filename, + subfolder: file.subfolder || '', + type: file.type || 'output' + } + for (const path of ['/delete', '/aigen/purge']) { + try { + const res = await comfyFetch(path, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }) + if (res.ok) return true + } catch { + /* Comfy may not expose this route */ + } + } + return false +} + async function purgeOnDesktop(opts: { - video?: { filename: string; subfolder: string; type: string } + video?: ComfyDiskFile + audio?: ComfyDiskFile + output?: ComfyDiskFile imageName?: string imageSubfolder?: string extraImageNames?: string[] @@ -558,9 +605,12 @@ async function purgeOnDesktop(opts: { imageSubfolder: opts.imageSubfolder || '', imageNames: (opts.extraImageNames || []).filter(Boolean), video: opts.video || null, - output: opts.video && /\.(png|jpe?g|webp|gif)$/i.test(opts.video.filename) ? opts.video : null + audio: opts.audio || null, + output: opts.output || (opts.video && /\.(png|jpe?g|webp|gif)$/i.test(opts.video.filename) ? opts.video : null), + sweepPrefixes: comfySweepNamePrefixes(), + sweepSubfolders: ['video', 'audio', 'image', 'still'] }), - signal: AbortSignal.timeout(8000) + signal: AbortSignal.timeout(12000) }) } catch { // Host agent may be down; Comfy HTTP purge is the fallback. @@ -568,7 +618,9 @@ async function purgeOnDesktop(opts: { } export async function purgeComfyArtifacts(opts: { - video?: { filename: string; subfolder: string; type: string } + video?: ComfyDiskFile + audio?: ComfyDiskFile + output?: ComfyDiskFile imageName?: string imageSubfolder?: string extraImageNames?: string[] @@ -576,23 +628,17 @@ export async function purgeComfyArtifacts(opts: { }) { if (!purgeComfyEnabled()) return await purgeOnDesktop(opts) - const files: { filename: string; subfolder: string; type: string }[] = [] + const files: ComfyDiskFile[] = [] if (opts.video?.filename) files.push(opts.video) + if (opts.audio?.filename) files.push(opts.audio) + if (opts.output?.filename) files.push(opts.output) if (opts.imageName) files.push({ filename: opts.imageName, subfolder: opts.imageSubfolder || '', type: 'input' }) for (const name of opts.extraImageNames || []) { if (!name || name === opts.imageName) continue files.push({ filename: name, subfolder: opts.imageSubfolder || '', type: 'input' }) } for (const file of files) { - try { - await comfyFetch('/aigen/purge', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(file) - }) - } catch { - // Comfy may not have the purge route loaded yet; never fail the saved clip. - } + await deleteComfyHttpFile(file) } if (opts.promptId) { try { diff --git a/server/utils/imageChain.ts b/server/utils/imageChain.ts index 3153e59..0e32529 100644 --- a/server/utils/imageChain.ts +++ b/server/utils/imageChain.ts @@ -1,6 +1,6 @@ import { createJob, emitJob, type Job } from '~/server/utils/jobs' import { ensureComfyReady } from '~/server/utils/comfyLifecycle' -import { assertImageScaleToTotalPixelsNode, getComfyHost, uploadImage, queuePrompt } from '~/server/utils/comfy' +import { assertImageScaleToTotalPixelsNode, comfyImagePrefix, getComfyHost, uploadImage, queuePrompt } from '~/server/utils/comfy' import { withImageComfyHost, waitForImageEdit, downloadEditedImage, purgeImageComfyArtifacts } from '~/server/utils/imageComfy' import { buildEditWorkflow } from '~/server/utils/imageWorkflow' import { ensureComfyLoraNames } from '~/server/utils/loras' @@ -128,7 +128,7 @@ export async function runEdit(job: Job, params: EditRunParams) { steps: params.steps, seed, cfg: params.cfg, - filenamePrefix: `${comfyJobPrefix(job.id)}_p${index + 1}`, + filenamePrefix: `${comfyImagePrefix()}_${comfyJobPrefix(job.id)}_p${index + 1}`, loraStack: params.loraStack || params.loraName, scaleToTotalPixels: params.scaleToTotalPixels, scaleMegapixels: params.scaleMegapixels diff --git a/server/utils/imageChainV2.ts b/server/utils/imageChainV2.ts index c88b447..1eb3c31 100644 --- a/server/utils/imageChainV2.ts +++ b/server/utils/imageChainV2.ts @@ -1,6 +1,6 @@ import { createJob, emitJob, type Job } from '~/server/utils/jobs' import { ensureComfyReady } from '~/server/utils/comfyLifecycle' -import { assertImageScaleToTotalPixelsNode, getComfyHost, uploadImage, queuePrompt } from '~/server/utils/comfy' +import { assertImageScaleToTotalPixelsNode, comfyImagePrefix, getComfyHost, uploadImage, queuePrompt } from '~/server/utils/comfy' import { withImageComfyHost, waitForImageEdit, downloadEditedImage, purgeImageComfyArtifacts } from '~/server/utils/imageComfy' import { buildImageV2Workflow, IMAGE_V2_NODE_LABELS, resolveKreaGenerateAssets } from '~/server/utils/imageWorkflowV2' import { ensureComfyLoraNames, persistLoraFields } from '~/server/utils/loras' @@ -243,7 +243,7 @@ export async function runEditV2(job: Job, params: EditV2RunParams) { height: params.height, aspect: params.aspect, turbo: params.turbo === true, - filenamePrefix: chainTotal > 1 ? `${comfyJobPrefix(job.id)}_p${index + 1}` : comfyJobPrefix(job.id), + filenamePrefix: chainTotal > 1 ? `${comfyImagePrefix()}_${comfyJobPrefix(job.id)}_p${index + 1}` : `${comfyImagePrefix()}_${comfyJobPrefix(job.id)}`, kreaUnetName: krea?.unet, kreaClipName: krea?.clip, kreaVaeName: krea?.vae, diff --git a/server/utils/imageComfy.ts b/server/utils/imageComfy.ts index 6101e58..74dba2f 100644 --- a/server/utils/imageComfy.ts +++ b/server/utils/imageComfy.ts @@ -1,5 +1,5 @@ import { AsyncLocalStorage } from 'node:async_hooks' -import { getComfyHost, viaAgentFrontDoor } from '~/server/utils/comfy' +import { getComfyHost, viaAgentFrontDoor, comfySweepNamePrefixes } from '~/server/utils/comfy' import { comfyJobPrefix } from '~/utils/outputNames' const imageHostAls = new AsyncLocalStorage() @@ -369,8 +369,12 @@ async function purgeImageOnDesktop(opts: { output?: { filename: string; subfolder: string; type: string } }) { const config = useRuntimeConfig() - const controlUrl = String(config.comfyControlUrl || process.env.COMFY_CONTROL_URL || '').replace(/\/$/, '') - const token = String(config.comfyControlToken || process.env.COMFY_CONTROL_TOKEN || '') + const controlUrl = String( + config.imageComfyControlUrl || process.env.IMAGE_COMFY_CONTROL_URL || config.comfyControlUrl || process.env.COMFY_CONTROL_URL || '' + ).replace(/\/$/, '') + const token = String( + config.imageComfyControlToken || process.env.IMAGE_COMFY_CONTROL_TOKEN || config.comfyControlToken || process.env.COMFY_CONTROL_TOKEN || '' + ) if (!controlUrl) return try { await fetch(`${controlUrl}/purge`, { @@ -384,7 +388,9 @@ async function purgeImageOnDesktop(opts: { imageName: opts.inputName || '', imageSubfolder: opts.inputSubfolder || '', imageNames: (opts.extraInputNames || []).filter(Boolean), - output: opts.output || null + output: opts.output || null, + sweepPrefixes: comfySweepNamePrefixes(), + sweepSubfolders: ['video', 'audio', 'image', 'still'] }), signal: AbortSignal.timeout(8000) }) diff --git a/server/utils/library.ts b/server/utils/library.ts index acf25c6..9c577b3 100644 --- a/server/utils/library.ts +++ b/server/utils/library.ts @@ -2087,6 +2087,7 @@ export async function importMissingComfyAudio(owner: string, folderId?: string) comfyFilename: item.audio.filename }) imported.push(track) + await purgeComfyArtifacts({ audio: item.audio, promptId: item.promptId }) } return imported } diff --git a/server/utils/musicChain.ts b/server/utils/musicChain.ts index 6998e27..552f061 100644 --- a/server/utils/musicChain.ts +++ b/server/utils/musicChain.ts @@ -1,5 +1,5 @@ import { createJob, emitJob, type Job } from '~/server/utils/jobs' -import { extractAudio, fetchHistory, fetchHistoryAll, findHistoryAudio, queuePrompt } from '~/server/utils/comfy' +import { extractAudio, fetchHistory, fetchHistoryAll, findHistoryAudio, purgeComfyArtifacts, queuePrompt } from '~/server/utils/comfy' import { comfyWsUrl } from '~/server/utils/comfy' import { ensureComfyReady } from '~/server/utils/comfyLifecycle' import { downloadComfyAudio, saveTrack } from '~/server/utils/library' @@ -195,6 +195,10 @@ function watchMusicJob(job: Job): Promise { }) job.trackId = track.id job.library.audioExt = ext + await purgeComfyArtifacts({ + audio, + promptId: job.promptId + }) } job.status = 'complete' emitJob(job, {