comfyui-simple-batch
Custom nodes for ComfyUI, focused on batch processing of images, audio and video files. Simple usage: replace the load/save nodes in the workflow, point to the input and output folders and run.
Nodes (6)
ComfyUI Simple Batch
Batch-process every file in a folder tree — images, audio, or video — through an existing single-item ComfyUI workflow, with no models reloaded between items and with crash-recovery that just works.
Built for a SeedVR2 + NVIDIA VFX upscale workflow, but fully workflow-agnostic: any graph between a Load node and its Save node works.
Six nodes (one Load/Save pair per media type, prefix-named; 📥 = source, 📤 = output):
| Node | Class name | Media | What it does |
| --- | --- | --- | --- |
| 📥 Batch Load Image | BatchLoadImage | Image | Reads each image in the source folder tree into an IMAGE (plus MASK) tensor for the workflow. |
| 📤 Batch Save Image | BatchSaveImage | Image | Writes the workflow's processed image to the mirrored destination tree. |
| 📥 Batch Load Audio | BatchLoadAudio | Audio | Reads each audio file into the standard ComfyUI AUDIO dict. |
| 📤 Batch Save Audio | BatchSaveAudio | Audio | Encodes and writes the processed audio back to the destination container. |
| 📥 Batch Load Video | BatchLoadVideo | Video | Exposes each video file as a lazy VIDEO container, like core LoadVideo. |
| 📤 Batch Save Video | BatchSaveVideo | Video | Writes the processed video with core SaveVideo semantics. |
- The Load node replaces the matching core Load node (
LoadImage/LoadAudio/LoadVideo) as the source. - The Save node replaces the matching core Save node (
SaveImage/SaveAudio/SaveVideo) at the end of the chain. js/batch_watchdog.jskeeps the batch alive when an item's processing crashes before the Save node ever runs — this is the load-bearing failure path, not optional plumbing.js/folder_browser.jsadds a Browse… button to every Load node'sfolder_pathand every Save node'soutput_rootwidget — an in-app folder picker (overlay panel, no native OS dialog) styled with ComfyUI's own design tokens.
Requirements
- ComfyUI with the V3 API (
comfy_api.latest) — the pack registers via the V3 extension entrypoint in__init__.pyand relies on V3 prompt-executor behavior, so a recent ComfyUI build is required. - Python ≥ 3.10 (declared in
pyproject.toml). - No pip install needed — the pack is installed by placing the folder in
custom_nodes/and only uses APIs ComfyUI already provides. It declarescomfyuias a dependency purely for Registry metadata. - PyAV is used by Batch Save Audio for FLAC / MP3 / WAV encoding and by Batch Save Video for stream-copying; ComfyUI already bundles it.
Install
Clone into ComfyUI's custom_nodes/ directory and restart ComfyUI — the
pack lives at the repository root, so a plain clone installs it:
git clone https://github.com/lordekeen/comfyui-simple-batch.git custom_nodes/comfyui-simple-batch
Or copy the repository into custom_nodes/comfyui-simple-batch/:
custom_nodes/
└── comfyui-simple-batch/
├── __init__.py
├── nodes.py
├── pyproject.toml
├── README.md
├── icon.png
├── examples/
└── js/
├── batch_watchdog.js
└── folder_browser.js
No pip install required — the pack only uses APIs ComfyUI already provides.
ComfyUI Registry / Manager: once published to the ComfyUI Registry, this pack can also be installed by searching for ComfyUI Simple Batch in the ComfyUI Manager and clicking Install — no manual clone required.
Examples
Four ready-to-run workflow JSONs live in examples/. Each is a
self-contained Batch Load → Batch Save passthrough graph (no processing in
between), ready to be used as the source and sink of an existing workflow:
| Example | Demonstrates |
| --- | --- |
| batch_image_example.json | Image batch: BatchLoadImage → BatchSaveImage. |
| batch_audio_example.json | Audio batch: BatchLoadAudio → BatchSaveAudio. |
| batch_video_example.json | Video batch: BatchLoadVideo → BatchSaveVideo. |
| batch_all_types_example.json | All three media pairs in one graph (image + audio + video). |
To load an example: drag the JSON file onto the ComfyUI canvas, or use the
Load button (Workflow menu / keyboard shortcut). The graphs contain
placeholder folder paths for folder_path and output_root — replace
them with the real folders on your machine before hitting Queue.
Usage
-
Add the Batch Load node matching your media type where the core Load node was, and the matching Batch Save node where the core Save node was.
-
Wire
SLOT,TOTAL, andRELATIVE_PATHfrom the Load node into the Save node (in addition to the media chain). -
Configure:
Batch Load Image / Audio / Video (identical widget sets):
folder_path— root of the input folder tree (absolute path).recursive— walk subfolders (default on).container_format— one format per run, or theall_*category:- Image:
png/jpg/jpeg/webp/bmp/tiff/all_images(defaultpng). - Audio:
wav/mp3/flac/ogg/m4a/opus/all_audio(defaultwav). - Video:
mp4/mov/mkv/webm/avi/all_videos(defaultmp4). Files of any other type underfolder_pathare silently ignored — not logged, not counted.
- Image:
Batch Save Image / Audio / Video:
output_root— root of the destination tree (absolute path, required).filename_template— how each output file is named, applied to the file's stem (the part before the extension). Default{filename}(reproduces the mirrored source name exactly). See "Filename template".format— output container,auto(default, keeps the source container and extension) or a forced format:- Image:
auto/png/jpg/webp. - Audio:
auto/flac/mp3/wav/opus. - Video:
auto/mp4(mirrors core SaveVideo's format selection).
- Image:
- Video only:
codec—auto(default, stream-copies when possible) orh264. - Both
folder_path(Load nodes) andoutput_root(Save nodes) have a Browse… button that opens an in-app folder picker — navigate the directory tree and select a folder without typing a path. Folder rows carry a 📁 icon and drill down on click; file rows carry a 📄 icon and are shown as context (they are never selectable — the widget stores a folder path). The picker styles itself with ComfyUI's own design tokens, so it follows your theme.
-
Press Queue. Each item prints progress to the console:
[slot+1/total] relative_path — saved -> <dest>/— skipped (already exists), and the last item prints a final summary line.
Media outputs
- Batch Load Image emits the image as an
IMAGEtensor[1, H, W, 3](RGB float32,0..1) plus aMASKtensor[1, H, W]derived from the alpha channel (1.0 - alpha, ComfyUI convention) — a zero[1, 64, 64]mask when the source has no alpha. Animated formats load every same-sized frame into the batch. Output metadata (prompt/workflow) is embedded in PNG only; WebP saves print a note that no metadata is written. - Batch Load Audio emits the standard ComfyUI
AUDIOdict ({"waveform": [1, C, N] float32, "sample_rate": int}), normalized to[-1, 1], one row per channel — mono and stereo sources survive the round-trip (packed/interleaved decodes such as WAV/FLAC are reshaped to[1, C, N], never flattened into a doubled mono channel). Batch Save Audio encodes it back to the target container (FLAC / MP3 / WAV via PyAV) keeping channel count, sample rate, and duration. - Batch Load Video emits a lazy
VIDEOcontainer (like core LoadVideo). Batch Save Video writes it with core SaveVideo semantics (format/codeccombos, stream-copy when possible, metadata embedding).
The destination tree mirrors the source tree (output_root +
relative_path) by default, so same-named files in different subfolders never
collide. The filename_template widget can change the stem of each output
name (see below) while the subfolder structure is always preserved.
Keep
output_rootout offolder_path(or anywhere else inside it). The pack actively excludesoutput_rootfrom the source enumeration, so even when the output folder lives inside the scanned tree its files are never re-fed into the batch — but a separate folder is still cleaner.
Filename template
filename_template controls how each output file is named. It is applied to
the file's stem only (the part before the extension):
{filename}— the original file stem, without its extension.{date}— the current date,YYYY-MM-DD.- Any other
{token}is left literal in the output name — never an error.
The subfolder path is always preserved verbatim, so the destination tree
still mirrors the source tree's structure. The extension is always appended
last, from the format widget (never controlled by the template): auto
keeps the source extension; a non-auto format remaps it (pic.png →
pic.webp for images, sound.wav → sound.flac for audio, clip.mkv →
clip.mp4 for video).
The default template is {filename}, which reproduces today's naming
byte-for-byte — existing workflows are unaffected.
- If your template omits
{filename}(e.g.{date}or a fixed name), two items in the same subfolder can resolve to the same output name. The first saves and the second is then seen as already handled and silently skipped. The pack appends one warning line to_simple_batch_log.txtper distinct template (not per item) so the risk is visible — it never blocks saving. - If your template carries its own extension (e.g.
{filename}.bakwithformat=mp4), theformatextension is still appended after it (clip.bak.mp4). That is your responsibility, not blocked.
Memory behavior (why the batch won't fill your RAM)
Every item re-runs the graph with changed inputs, so every node downstream of the Load node produces a new cache entry per item — and in a video workflow those entries are multi-GB frame batches. ComfyUI's default cache never prunes re-keyed entries (it only releases them under allocation pressure, which the Linux OOM killer can beat), so an unmanaged batch fills system RAM after an item or two.
This pack manages it: at the start of every item, the Load node releases all cached outputs except the nodes upstream of itself — the model loaders and other constant nodes, whose cache keys are stable across items and therefore stay resident (the "models untouched between runs" guarantee). Only one item's intermediates are ever in RAM at a time, and the release runs on the executor's own worker thread at the moment the previous item's tensors are no longer needed.
Mechanically, ComfyUI does not expose its prompt executor, so the pack
records it by wrapping PromptExecutor.__init__ at import time (the same
kind of wrapper other established packs use, e.g. impact-pack wraps
PromptExecutor.handle_execution_error). If that hook ever fails to
install, the batch still works and only the RAM hygiene degrades — with a
warning logged.
Resume / re-run / new files — no reset step
The Load node rescans the folder tree on every single execution and derives the slot from the destination tree's existence check. That single mechanism makes all of these just work:
- resuming after a ComfyUI restart;
- re-running the same folder (already-handled files are skipped);
- adding new files to the folder between runs (they get picked up);
- crash recovery (see below).
How the batch works
ComfyUI graphs are DAGs — there are no in-graph loops. The batch illusion is created by the Save node re-submitting the same prompt to the server's queue after each item finishes until every file has been handled:
flowchart TD
A([Scan folder tree<br/>recursive + slot check]) --> B{Unhandled file?}
subgraph LOAD["Batch Load"]
C[Load node emits item<br/>+ SLOT / TOTAL / RELATIVE_PATH]
end
subgraph WORK["Your workflow"]
D[Existing nodes<br/>process the item]
end
subgraph SAVE["Batch Save + watchdog"]
E[Save node writes result<br/>to mirrored tree]
F{Item completed?}
G[Watchdog catches<br/>execution_error]
H[POST /simple_batch/failure<br/>log + .error marker]
end
B -- "no" --> Z([End<br/>TOTAL reached])
B -- "yes" --> C
C --> D
D --> E
E --> F
F -- "yes, more remain" --> B
F -- "yes, last item" --> Z
D -. "crash before Save" .-> G
G --> H
H -- "skip item, continue" --> B
[Batch Load Image] --IMAGE--> [...existing workflow...] --IMAGE--> [Batch Save Image]
| |
+------------------- relative_path (STRING) --------------------->+
Because it is the same long-running ComfyUI server process across all submissions, model-loading nodes upstream (SeedVR2 DiT/VAE loaders, the VFX upscaler, any checkpoint/CLIP/VAE loader) are untouched between runs, and ComfyUI's node-output caching keeps them resident in VRAM — native behavior, nothing re-implemented here.
There are two triggers for the same underlying loop (front-of-queue insertion of the identical prompt JSON):
| Trigger | When | Who | | --- | --- | --- | | Happy path | an item finishes and more remain | the Save node re-queues from inside the running prompt | | Failure path | a node upstream of the Save node crashes (OOM, corrupt input, …) | the JS watchdog reports it, the server logs + marks the slot + re-queues |
Both insert at the front of the queue (ahead of any unrelated jobs the user queued meanwhile — the batch keeps priority over itself), using the same mechanism as ComfyUI's own "run at front" prompt submission.
Failure handling (how the batch survives a crash)
The problem: if a node upstream of the Save node throws (VFX node OOMs, corrupt input file, SeedVR2 crashes on a malformed clip), ComfyUI halts the whole prompt. The Save node never runs — it can't log, can't advance the slot, can't requeue. Error handling inside the Save node is unreachable exactly when it's needed most. That is why the JS watchdog exists.
The fix — two distinguishable server signals:
- Genuine crash → ComfyUI broadcasts
execution_error. The watchdog (js/batch_watchdog.js) catches it and POSTs the failed prompt id to the pack's/simple_batch/failureroute. The server:- resolves the failed prompt from its queue running/history state;
- verifies it is one of our batch prompts (contains a Batch Load + Batch Save pair) — unrelated workflow errors are ignored;
- recomputes the failed item's slot from the destination tree (unchanged, since the item never produced output);
- appends one line to
_simple_batch_log.txtinsideoutput_root(created if needed, appended forever, never rotated):2026-01-01 12:00:00 | slot 3/50 | subdir/clip.mp4 | RuntimeError: CUDA out of memory; - writes a zero-byte
<destination>.errormarker; - re-submits the same prompt at the front of the queue — the next run sees the marker, skips the bad item, and continues with the next one (one attempt per item, then logged and skipped).
- User-initiated Cancel → ComfyUI broadcasts
execution_interruptedinstead. The watchdog does nothing: no log, no requeue. The batch simply stops — that is the intended behavior when you hit Cancel.
⚠️ Implementation notes (for reviewers)
These are the decisions made where the spec left room ("open implementation detail for the coding agent"):
- Error marker convention. A failed item is marked by a zero-byte file at
<output_root>/<relative_path>.error— right next to where the output would have been written.is_handled()(one shared function) treats a destination as handled when the file or its.errormarker exists, so the Load node's slot computation and the Save node's skip-if-exists check are literally the same mechanism ("the two are the same mechanism, not two"). To re-process an item that failed, delete its.errormarker (and its output if any). - Partial outputs from a killed process. A zero-byte destination never counts as handled (a real output can't be empty; an empty file is what an interrupted write leaves behind), so the item is retried after a crash. A truncated-but-non-empty file still counts as handled — that is the spec's plain existence rule, kept deliberately simple.
- Slot derivation. The Load node walks the sorted file list to the first unhandled destination. Under strictly sequential processing (the supported mode) this is exactly "counting how many files already have an output" — the spec's wording — and it stays correct when a newly added file sorts ahead of already-processed ones.
- JS watchdog delegates to a server route. The spec describes the
watchdog itself appending the log line and re-submitting the prompt. In
this implementation the watchdog remains the sole trigger (event
detection + cancel/crash distinction + dedupe), while the log write, the
marker write, and the front-of-queue requeue happen server-side at
POST /simple_batch/failure. This keeps every file/queue operation on the server (where the filesystem and the queue live) and guarantees both triggers reuse the exact same requeue code path. - Config discovery from the graph. The Load node has no
output_rootwidget (per the spec, all filtering/recursion config lives on the Load node and the Save node owns the destination root). It reads the Save node'soutput_root/format/filename_templatevalues out of the shared prompt graph (hiddenPROMPTinput), so its existence check and the Save node's write target can never disagree. - Front-of-queue insertion. ComfyUI's queue is a min-heap ordered by a
per-prompt number; the server's own "front" submission negates it. Both
triggers do the same (
-(number)), which sorts the re-submitted prompt ahead of any user jobs — matching core behavior. - Cache busting. Every Load node overrides
fingerprint_inputs(the V3 equivalent ofIS_CHANGED) to return a value that changes on every call, so it rescans on every execution — including each self-requeue — and never serves a stale cached item. The requeue paths deliberately re-submit the pre-execution snapshot from ComfyUI's queue (currently_running/ history), which never carries the executor'sis_changedmutations — a mutated prompt would carry the old fingerprint and cache-hit the previous item (batch stall). - Per-item cache release. See "Memory behavior" above — the executor wrapper plus selective eviction of everything downstream of the Load node, so only the current item's intermediates ever occupy RAM.
Backward compatibility
LoadFolder / SaveFile remain as Python aliases of the video pair
(BatchLoadVideo / BatchSaveVideo), and graph inspection
(find_batch_nodes, the failure route, get_save_config) still recognizes
the legacy class names in older saved prompts — so crash recovery and slot
bookkeeping keep working for old workflows.
The extension registers exactly the six BatchLoad* / BatchSave* node
names, so an older workflow saved with LoadFolder / SaveFile class types
must be re-created with the new nodes before it will run in a fresh session
(anything that maps the legacy names — a custom loader, or
NODE_CLASS_MAPPINGS entries — works unchanged, as the smoke suite verifies).
Non-goals (v1)
- Concurrent multi-folder batches (not actively broken, just not designed for).
- Retry logic for failed items (one attempt, then logged and skipped).
- Any UI beyond console + log file.
- Source containers other than the one selected in
container_formatper run (others silently ignored). - Enforcing filename-template uniqueness (warned in the log, not blocked).
Progress / UI (phased)
- Phase 1 (this release): console print per item (
[slot+1/total] relative_path — status) +_simple_batch_log.txtfor errors. Pure Python/JS, no custom widget UI. - Phase 2 (stretch): live-updating widget on the Load node showing per-slot status (pending/done/skipped/error). Separate, later PR.
Screenshots
Screenshots and UI captures are welcome and help the Registry listing! Good candidates:
- The Browse… folder picker open on a Load or Save node.
- A running batch's console progress (
[slot+1/total] ... saved -> <dest>). - The Batch Load → existing workflow → Batch Save graph wired up.
Drop the images into docs/screenshots/ and link them here in a pull request.
A 512×512 icon.png ships at the repository root for the Registry
listing.
Reference workflow
LoadVideo → GetVideoComponents → JoinImageWithAlpha → SeedVR2VideoUpscaler (ainvfx/ComfyUI-SeedVR2_VideoUpscaler) → [NVIDIA VFX upscale node] → CreateVideo → SaveVideo
flowchart LR
A[Batch Load Video<br/>replaces LoadVideo] --> B[GetVideoComponents]
B --> C[JoinImageWithAlpha]
C --> D[SeedVR2VideoUpscaler]
D --> E[NVIDIA VFX upscale node]
E --> F[CreateVideo]
F --> G[Batch Save Video<br/>replaces SaveVideo]
Batch Load Video replaces LoadVideo; Batch Save Video replaces SaveVideo.
License
GPL-3.0-only. See the LICENSE file for the full license text.