Files
ausboss-ComfyUI-AusBoss/tests/test_align_helpers.py
ausboss 05038baaa3 Five basics the normal nodes have: sums, any wire into Show Text, name tokens, word weights, Image Size's numbers
Found in a sweep of the pack against ComfyUI's own nodes: things the normal
node does that the pack's node did not.

- Every number box takes a sum. Click a number, type 1024*1.5 or
  (1920-64)/2 and press Enter, as in ComfyUI's own number boxes: + - * / %,
  brackets and a sign. The reader is the pack's own (sumValue in
  js/shared/scrub_input.mjs) and is only asked when the text is not a plain
  number.
- Show Text takes any wire. Its input was STRING only. A number, an on/off
  value or a list is shown too; a picture or a latent is shown by its size.
  The output stays STRING and carries the text that is shown, so text
  passes through unchanged and a number comes out as text.
- Save Image takes name tokens in the file name, as ComfyUI's own Save
  Image does: %date:yyyy-MM-dd% and %Node.setting% are filled in the
  browser when a run is sent (js/shared/name_tokens.mjs, now shared with
  Save Video), %width% %height% and the time tokens on the server. The
  path preview shows them. A linked filename is never read for tokens.
- Ctrl + Up and Ctrl + Down change a word's weight in the pack's prompt
  boxes (Text, Krea 2 Encode). The card's text box stopped the key before
  ComfyUI's own listener saw it; those two keys now go on from a multiline
  box, and from nowhere else.
- Image Size shows what it measured beside its outputs after a run
  ("width: 1024"). The numbers go into the output labels, so the node keeps
  its size. ComfyUI says nothing about a node whose answer came from its
  cache, so the numbers are kept in the node's properties and come back
  when a workflow is opened again; a changed picture wire clears them.

No input was added, renamed or reordered. Help pages, README lines, tests
and CHANGELOG entries are included.
2026-10-11 17:10:52 -05:00

166 lines
7.4 KiB
Python

"""Offline tests for Align Image and Image Size."""
from __future__ import annotations
import sys
import unittest
from pathlib import Path
import torch
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
if "nodes" in sys.modules and not hasattr(sys.modules["nodes"], "__path__"):
del sys.modules["nodes"]
from nodes._align_helpers import ALIGN_MODES, align_image, aligned_size
from nodes.node_align_image import AusBossAlignImage
from nodes.node_image_size import AusBossImageSize
class AlignedSizeTests(unittest.TestCase):
def test_crop_rounds_down_pad_rounds_up_resize_rounds_nearest(self):
# 1000 / 16 = 62.5: down 992, up 1008, nearest ties-up 1008.
self.assertEqual(aligned_size(1000, 1000, 16, "crop"), (992, 992))
self.assertEqual(aligned_size(1000, 1000, 16, "pad"), (1008, 1008))
self.assertEqual(aligned_size(1000, 1000, 16, "resize"), (1008, 1008))
# 1030 is nearer 1024 than 1040.
self.assertEqual(aligned_size(1030, 500, 16, "resize"), (1024, 496))
def test_already_aligned_sizes_pass_through_every_mode(self):
for mode in ALIGN_MODES:
self.assertEqual(aligned_size(1024, 768, 32, mode), (1024, 768))
def test_a_side_smaller_than_one_multiple_snaps_up_to_one(self):
# Nothing can crop below the smallest legal size.
for mode in ALIGN_MODES:
self.assertEqual(aligned_size(20, 100, 32, mode)[0], 32, mode)
def test_multiple_one_changes_nothing(self):
for mode in ALIGN_MODES:
self.assertEqual(aligned_size(1013, 777, 1, mode), (1013, 777))
def test_unknown_mode_raises(self):
with self.assertRaises(ValueError):
aligned_size(100, 100, 16, "stretch")
def ramp(batch: int, height: int, width: int) -> torch.Tensor:
"""A BHWC batch whose pixel values encode their own x coordinate."""
xs = torch.arange(width, dtype=torch.float32) / max(1, width - 1)
return xs.view(1, 1, -1, 1).expand(batch, height, width, 3).clone()
class AlignImageTests(unittest.TestCase):
def test_resize_hits_the_nearest_multiple(self):
out, width, height, ox, oy = align_image(ramp(2, 500, 1000), 16, "resize")
self.assertEqual((width, height), (1008, 496))
self.assertEqual(tuple(out.shape), (2, 496, 1008, 3))
self.assertEqual((ox, oy), (0, 0))
def test_crop_is_centered_and_lossless_in_the_kept_region(self):
frames = ramp(1, 100, 100)
out, width, height, ox, oy = align_image(frames, 32, "crop")
self.assertEqual((width, height), (96, 96))
# Center crop of 100 -> 96 drops 2 columns each side.
self.assertTrue(torch.equal(out, frames[:, 2:98, 2:98, :]))
# The original's corner now sits 2 px outside the output.
self.assertEqual((ox, oy), (-2, -2))
def test_pad_replicates_edges_up_to_the_next_multiple(self):
frames = ramp(1, 100, 100)
out, width, height, ox, oy = align_image(frames, 32, "pad")
self.assertEqual((width, height), (128, 128))
# 28 extra pixels split 14/14; the original sits centered, untouched.
self.assertTrue(torch.equal(out[:, 14:114, 14:114, :], frames))
# The offsets point exactly at where the original landed.
self.assertEqual((ox, oy), (14, 14))
self.assertTrue(
torch.equal(out[:, oy : oy + 100, ox : ox + 100, :], frames)
)
# Padding replicates the border, so corners equal the corner pixel.
self.assertTrue(torch.equal(out[0, 0, 0], frames[0, 0, 0]))
self.assertTrue(torch.equal(out[0, -1, -1], frames[0, -1, -1]))
def test_already_aligned_input_is_returned_as_the_same_object(self):
frames = ramp(1, 64, 128)
out, width, height, _ox, _oy = align_image(frames, 32, "crop")
self.assertIs(out, frames)
self.assertEqual((width, height), (128, 64))
def test_crop_mode_pads_a_side_it_cannot_crop(self):
# 20 px wide with multiple 32: crop cannot reach a multiple below,
# so that side replicate-pads up to exactly one multiple.
out, width, height, ox, oy = align_image(ramp(1, 100, 20), 32, "crop")
self.assertEqual((width, height), (32, 96))
self.assertEqual(tuple(out.shape), (1, 96, 32, 3))
# Mixed axes: the padded side is positive, the cropped side negative.
self.assertEqual((ox, oy), (6, -2))
def test_color_pad_keeps_an_alpha_channel_opaque(self):
frames = torch.cat([ramp(1, 20, 20), torch.full((1, 20, 20, 1), 0.25)], dim=-1)
out, width, height, ox, oy = align_image(
frames, 16, "pad", pad_fill="color", pad_color="#ff0000"
)
self.assertEqual((width, height), (32, 32))
self.assertEqual(tuple(out.shape), (1, 32, 32, 4))
self.assertTrue(torch.equal(out[:, oy : oy + 20, ox : ox + 20, :], frames))
# The padded border takes the color, and the alpha channel pads opaque.
self.assertTrue(torch.equal(out[0, 0, 0], torch.tensor([1.0, 0.0, 0.0, 1.0])))
self.assertTrue(torch.equal(out[0, -1, -1], torch.tensor([1.0, 0.0, 0.0, 1.0])))
def test_batch_and_values_survive(self):
frames = ramp(3, 40, 40)
out, _w, _h, _ox, _oy = align_image(frames, 16, "pad")
self.assertEqual(out.shape[0], 3)
self.assertGreaterEqual(float(out.min()), 0.0)
self.assertLessEqual(float(out.max()), 1.0)
def test_rejects_non_bhwc_input(self):
with self.assertRaises(ValueError):
align_image(torch.zeros((64, 64, 3)), 16, "resize")
class AlignImageNodeTests(unittest.TestCase):
def test_node_returns_image_and_size_only(self):
# The helper still reports offsets; the node's face stops at the
# three outputs a graph wires (offsets were dropped 2026-09-03).
node = AusBossAlignImage()
result = node.align(ramp(1, 100, 100), 32, "pad")
self.assertEqual(len(result), 3)
out, width, height = result
self.assertEqual((width, height), (128, 128))
self.assertEqual(tuple(out.shape), (1, 128, 128, 3))
for value in (width, height):
self.assertIsInstance(value, int)
self.assertEqual(AusBossAlignImage.RETURN_NAMES, ("image", "width", "height"))
def test_mode_choices_match_the_helper(self):
widget = AusBossAlignImage.INPUT_TYPES()["required"]["mode"][0]
self.assertEqual(tuple(widget), ALIGN_MODES)
class ImageSizeNodeTests(unittest.TestCase):
def test_reports_width_height_and_both_edges(self):
node = AusBossImageSize()
self.assertEqual(node.measure(torch.zeros((2, 480, 832, 3)))["result"], (832, 480, 832, 480, 2))
self.assertEqual(node.measure(torch.zeros((1, 1080, 608, 3)))["result"], (608, 1080, 1080, 608, 1))
def test_square_images_agree_on_both_edges(self):
node = AusBossImageSize()
self.assertEqual(node.measure(torch.zeros((1, 512, 512, 3)))["result"], (512, 512, 512, 512, 1))
def test_the_node_face_gets_the_same_numbers_as_the_outputs(self):
measured = AusBossImageSize().measure(torch.zeros((2, 480, 832, 3)))
self.assertEqual(measured["ui"], {"ausboss_size": [832, 480, 832, 480, 2]})
self.assertEqual(list(measured["result"]), measured["ui"]["ausboss_size"])
self.assertEqual(len(measured["result"]), len(AusBossImageSize.RETURN_TYPES))
def test_rejects_non_bhwc_input(self):
with self.assertRaises(ValueError):
AusBossImageSize().measure(torch.zeros((512, 512)))
if __name__ == "__main__":
unittest.main()