Add CLAUDE.md with architecture and local dev notes

Documents the V3 node registration path, the three encode layers, the
deliberate copy-not-import relationship with comfy_extras/nodes_images.py
(including where this copy diverges), and how to smoke-test the encoder
against the local ComfyUI checkout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3SFLThEvfvGqJDFKAf32B
This commit is contained in:
dreevelle
2026-08-08 17:50:07 +01:00
co-authored by Claude Opus 5
parent 11ae9a3718
commit 8d23559873
+99
View File
@@ -0,0 +1,99 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
A single-file ComfyUI custom node package. All code lives in `__init__.py`; there
is no build step, test suite, or linter config. The whole extension is one node,
`SaveImageSequenceFast`, which saves an image batch as a PNG sequence with the
frames encoded in parallel.
The node is registered through ComfyUI's **V3 schema API**, not the legacy
`NODE_CLASS_MAPPINGS` dict: `comfy_entrypoint()` returns a `ComfyExtension` whose
`get_node_list()` yields `IO.ComfyNode` subclasses. Inputs are declared in
`define_schema()` and arrive as named kwargs to `execute()`; hidden inputs
(`prompt`, `extra_pnginfo`) are read off `cls.hidden`, not from the signature.
## Local environment
- ComfyUI checkout: `/home/dreevelle/comfy/ComfyUI`
- Python venv (has `av`, `torch`, `numpy`): `/home/dreevelle/comfy-env/bin/python`
- The installed copy at `/home/dreevelle/comfy/ComfyUI/custom_nodes/ComfyUI-FastImageSequence`
is an **independent clone of the same GitHub remote**, not a symlink to this
working tree. Changes made here are not live in ComfyUI until they're pushed
and pulled there (or copied over).
Run ComfyUI:
```bash
/home/dreevelle/comfy-env/bin/python /home/dreevelle/comfy/ComfyUI/main.py
```
## Verifying changes
`__init__.py` imports `folder_paths`, `comfy.cli_args`, and `comfy_api.latest` at
module scope, so it cannot be imported standalone — ComfyUI must be on
`sys.path`. To exercise the pure-encoding helpers (`_encode_png`,
`inject_png_metadata`) without launching the server:
```bash
cd /home/dreevelle/comfy/ComfyUI && PYTHONPATH=/home/dreevelle/comfy/ComfyUI \
/home/dreevelle/comfy-env/bin/python -c "
import importlib.util, torch
spec = importlib.util.spec_from_file_location('fis', '/home/dreevelle/Projects/ComfyUI-FastImageSequence/__init__.py')
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
print(len(m._encode_png(torch.rand(8, 8, 3), '16-bit')))
"
```
The project's core invariant is **byte-identical output** to the built-in
`Save Image (Advanced)` node. Any change to the encode path should be checked by
encoding the same tensor through both `_encode_png` here and `_encode_image` in
`comfy_extras/nodes_images.py` and comparing the bytes.
## Architecture
Three layers in `__init__.py`, in order:
1. **`_encode_png`** — tensor → PNG bytes, entirely in memory. Quantizes to
uint8/uint16 via `_FORMAT_SPECS`, then encodes through a raw
`av.CodecContext` rather than a container: PyAV's `image2` muxer needs a real
file path and won't write to `BytesIO`, and for a single-frame PNG the codec
output *is* the file. Note the little-endian frame format / big-endian stream
format pairing for 16-bit — the `reformat()` call handles the swap.
2. **`inject_png_metadata`** — splices `tEXt` chunks in after IHDR by parsing the
IHDR length from bytes 8:12. Operates on finished PNG bytes, so it's
independent of the encoder.
3. **`SaveImageSequenceFast.execute`** — filename/counter allocation via
`folder_paths.get_save_image_path`, then a `ThreadPoolExecutor` over
`encode_and_write`. Parallelism works because PyAV releases the GIL during
zlib compression. `pool.map` is used specifically because it preserves input
order, keeping the returned UI results frame-ordered.
Because the node allocates one `counter` for the whole batch and derives each
filename as `counter + batch_number`, a single invocation owns a contiguous
numbering range. Splitting one sequence across two save nodes collides.
### Relationship to ComfyUI core
Layers 1 and 2 are **deliberately copied** from
`comfy_extras/nodes_images.py` rather than imported, so a core update can't
silently break this node. That means upstream changes don't propagate: when
touching the encode path, diff against that file.
The copy is also a subset. Upstream keys `_FORMAT_SPECS` by
`(file_format, bit_depth, num_channels)` and supports EXR, colorspace
conversion, and 1-channel grayscale (including 2D `HxW` tensors); here the key is
`(bit_depth, has_alpha)` and only 3- and 4-channel PNG exist. A grayscale input
raises `KeyError` — if that needs supporting, port the `num_channels`-keyed spec
table and the `ndim == 2` unsqueeze from upstream.
## Conventions
- Version is duplicated in `__version__` (`__init__.py`) and `[project].version`
(`pyproject.toml`) — bump both.
- `dependencies` stays empty: `av`, `numpy`, and `torch` ship with ComfyUI, and
the package is installed by cloning into `custom_nodes/`.
- Respect `args.disable_metadata` (ComfyUI's global CLI flag) alongside the
node's own `metadata` option in any metadata-related change.