Two nodes addressing distinct problems with ComfyUI's built-in
Resolution Selector (comfy_extras/nodes_resolution.py).
ResolutionSelectorMP
Core computes `megapixels * 1024 * 1024`, so every target is inflated
4.86% in area: 2.36 MP at 9:16 yields 1184x2096 = 2.48 real MP. This
uses 10^6.
Core also rounds each axis onto the grid independently, which drifts
the aspect ratio (that same result is 0.5649, not 0.5625). With
exact_ratio on, this solves instead on the lattice of sizes that are
simultaneously grid-aligned and exactly on-ratio: for a reduced ratio
a:b and alignment m those are (a*L*n, b*L*n) where
L = lcm(m / gcd(a, m), m / gcd(b, m))
9:16 at m=16 gives the lattice 144n x 256n, and n=8 lands on
1152x2048 -- exact 9:16, on the grid, 2.36 MP. solve_nearest()
preserves core's behaviour behind exact_ratio=False for when the
lattice is too coarse.
ResolutionAlignToGrid
Snaps arbitrary dimensions onto the grid (1920x1080 -> 1920x1088),
with round/up/down.
Alignment is vae_spatial_downscale * dit_patch_size, verified against
the ComfyUI source rather than assumed: 16 for Krea 2 (Wan21 inherits
spacial_downscale_ratio 8, patch 2), 32 for MiniMax H3
(spacial_downscale_ratio 16, patch_size (1,2,2)), 64 for SDXL/SD1.5.
Off-grid dimensions get circular-padded by
comfy.ldm.common_dit.pad_to_patch_size -- the pad row is a wrapped copy
of the top of the image, folded into the same patch token as the real
bottom row, which decodes to a band of artifacts along the bottom edge.
Padding is only ever appended, so the artifact is never top/left.
Verified: ruff clean; both schemas construct and both execute() paths
run; a 4000-case fuzz over random ratio/alignment/target triples asserts
w % m == 0, h % m == 0 and w*b == h*a (zero ratio drift); core's formula
reimplemented and reproduces the observed 1184x2096 exactly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CdgKVDqEbD14EJ4RC3GPmg
4.9 KiB
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. Two nodes,
ResolutionSelectorMP and ResolutionAlignToGrid, both pure integer maths with
no torch/numpy involvement.
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().
Local environment
-
ComfyUI checkout:
/home/dreevelle/comfy/ComfyUI -
Python venv:
/home/dreevelle/comfy-env/bin/python -
ComfyUI/custom_nodes/ComfyUI-ResolutionToolsis a symlink to this working tree, so edits are live on the next server restart:ln -sfn /home/dreevelle/Projects/ComfyUI-ResolutionTools \ /home/dreevelle/comfy/ComfyUI/custom_nodes/ComfyUI-ResolutionToolsThis is the pre-publish testing setup. Once the package is on the registry, swap the symlink for an independent clone of the GitHub remote (the FastImageSequence arrangement) so the installed copy tracks releases rather than the working tree.
Verifying changes
__init__.py imports comfy_api.latest at module scope, so it needs ComfyUI on
sys.path — but the maths functions are module-level and dependency-free, so
they can be exercised directly:
cd /home/dreevelle/comfy/ComfyUI && PYTHONPATH=/home/dreevelle/comfy/ComfyUI \
/home/dreevelle/comfy-env/bin/python -c "
import importlib.util
spec = importlib.util.spec_from_file_location('rt', '/home/dreevelle/Projects/ComfyUI-ResolutionTools/__init__.py')
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
print(m.solve_exact(9, 16, 16, 2.36e6)) # -> (1152, 2048)
"
Two invariants any change to solve_exact must preserve, and which are worth
re-fuzzing over random (ratio, alignment, target) triples:
width % alignment == 0 and height % alignment == 0width * h_ratio == height * w_ratio(ratio held exactly, no drift)
Known-good anchors: 9:16 @ m=16, 2.36 MP -> 1152x2048;
16:9 @ m=32, 1.33 MP -> 1536x864; 21:9 @ m=16, 2.37 MP -> 2352x1008.
Why the maths is what it is
Real megapixels. Core's comfy_extras/nodes_resolution.py:78 uses
megapixels * 1024 * 1024. That's the SDXL-era convention where "1 MP" meant
1024x1024. This package deliberately diverges to * 1_000_000. Do not "fix"
this to match core — the divergence is the point, and the README documents it.
Lattice solving. lattice_step() computes the smallest scale increment that
keeps both axes on the alignment grid while holding an exact ratio. Core rounds
each axis independently, which drifts the ratio (9:16 at 2.36 MP becomes 0.5649
instead of 0.5625). solve_nearest() preserves core's behaviour for when the
lattice is too coarse; it is opt-in via exact_ratio=False.
Alignment = VAE spatial downscale x DiT patch size. Verified against the ComfyUI source rather than assumed:
- Krea 2 -> 16.
latent_formats.LatentFormat.spacial_downscale_ratio = 8(inherited byWan21, whichsupported_models.Krea2uses) xpatch=2incomfy/ldm/krea2/model.py. - MiniMax H3 -> 32.
latent_formats.MiniMaxH3Video.spacial_downscale_ratio = 16xpatch_size=(1, 2, 2)incomfy/ldm/minimax/model.py. - SDXL/SD1.5 -> 64. UNet, no patchify; three downsample stages on an 8x VAE.
The failure mode when unaligned is comfy.ldm.common_dit.pad_to_patch_size,
which pads with padding_mode="circular" and appends only at the end — hence a
bottom/right band, never top/left. If a future model changes VAE or patch size,
the alignment table in the README and ALIGNMENT_TOOLTIP both need updating.
Conventions
- Version is duplicated in
__version__(__init__.py) and[project].version(pyproject.toml) — bump both. dependenciesstays empty; this package is stdlib-only by design.- Node IDs (
ResolutionSelectorMP,ResolutionAlignToGrid) are the public API. Renaming one breaks every saved workflow that uses it.
Releasing
Intended for the Comfy Registry as comfyui-resolution-tools under publisher
dreevelle, same setup as ComfyUI-FastImageSequence.
Changing [project].version in pyproject.toml on main publishes a
release. .github/workflows/publish_action.yml watches that path and runs
Comfy-Org/publish-node-action with the REGISTRY_ACCESS_TOKEN repo secret.
Don't touch the version field unless a release is intended; republishing an
existing version fails with 400 The node version already exists.
.comfyignore keeps CLAUDE.md and .github/ out of the published archive.
comfy node validate runs the registry checks locally; comfy node pack
produces the exact archive that would be uploaded.