Files
danrh 07f3dc254f Add model-specific Director nodes and grounded LTX/H3 workflows
Introduce model-specific target profiles, fail-closed grounding and validation, content-addressed Director caching, LTX-2.5 contracts, comparison workflows, benchmarks, schemas, documentation, and regression coverage.
2026-08-17 06:49:03 -05:00

166 lines
7.4 KiB
Python

from __future__ import annotations
import importlib.util
import json
import sys
import unittest
import uuid
from pathlib import Path
import torch
ROOT = Path(__file__).resolve().parents[1]
COMFY_ROOT = ROOT.parents[1]
def load_nodes_module():
if str(COMFY_ROOT) not in sys.path:
sys.path.insert(0, str(COMFY_ROOT))
package_name = f"diffusiongemma_h3_reference_prep_fixture_{uuid.uuid4().hex}"
spec = importlib.util.spec_from_file_location(
package_name,
ROOT / "__init__.py",
submodule_search_locations=[str(ROOT)],
)
if spec is None or spec.loader is None:
raise RuntimeError("could not load DiffusionGemma Prompt Builder")
package = importlib.util.module_from_spec(spec)
sys.modules[package_name] = package
spec.loader.exec_module(package)
return sys.modules[f"{package_name}.nodes"]
class H3ReferencePairPrepTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.nodes = load_nodes_module()
cls.node = cls.nodes.DiffusionGemmaH3ReferencePairPrep()
def test_reference_prep_nodes_are_exported_as_optional_nodes(self) -> None:
self.assertIs(
self.nodes.NODE_CLASS_MAPPINGS["DiffusionGemmaReferencePrep"],
self.nodes.DiffusionGemmaReferencePrep,
)
self.assertIs(
self.nodes.NODE_CLASS_MAPPINGS["DiffusionGemmaH3ReferencePairPrep"],
self.nodes.DiffusionGemmaH3ReferencePairPrep,
)
self.assertEqual(
self.nodes.DiffusionGemmaH3ReferencePairPrep.CATEGORY,
self.nodes.OPTIONAL_CATEGORY,
)
required = self.nodes.DiffusionGemmaH3ReferencePairPrep.INPUT_TYPES()["required"]
self.assertEqual(required["combined_reference_area_ratio"][1]["default"], 1.0)
self.assertEqual(required["reference_1_share"][1]["default"], 0.60)
def test_single_reference_prep_does_not_upscale_by_default(self) -> None:
image = torch.rand((1, 768, 768, 3), dtype=torch.float32)
prepared = self.nodes.DiffusionGemmaReferencePrep().prepare(
image,
"source",
"preserve_source",
1280,
8,
)
self.assertIs(prepared[0], image)
self.assertEqual((prepared[2], prepared[3]), (768, 768))
self.assertFalse(json.loads(prepared[1])["upscale_smaller_images"])
def test_current_square_and_portrait_pair_share_one_generation_frame_budget(self) -> None:
picture_1 = torch.zeros((1, 768, 768, 3), dtype=torch.float32)
picture_1[..., 0] = 1.0
picture_2 = torch.zeros((1, 1808, 1024, 3), dtype=torch.float32)
picture_2[..., 1] = 1.0
result = self.node.prepare(picture_1, picture_2, 1376, 768, 1.0, 0.60)
output_1, output_2 = result[0], result[1]
metadata = json.loads(result[2])
self.assertEqual(tuple(output_1.shape), (1, 768, 768, 3))
self.assertEqual(tuple(output_2.shape), (1, 896, 512, 3))
self.assertEqual((result[3], result[4]), (768, 768))
self.assertEqual((result[5], result[6]), (512, 896))
self.assertLessEqual(
int(output_1.shape[1] * output_1.shape[2] + output_2.shape[1] * output_2.shape[2]),
1376 * 768,
)
self.assertEqual(result[8], 1024)
self.assertEqual(metadata["native_match_estimated_packed_reference_rows"], 1608)
self.assertEqual(metadata["references"][0]["picture_tag"], "<Picture 1>")
self.assertEqual(metadata["references"][1]["picture_tag"], "<Picture 2>")
self.assertAlmostEqual(float(output_1[..., 0].mean()), 1.0, places=5)
self.assertAlmostEqual(float(output_2[..., 1].mean()), 1.0, places=5)
def test_outputs_are_downscale_only_aligned_and_preserve_each_aspect(self) -> None:
picture_1 = torch.rand((1, 777, 1234, 3), dtype=torch.float32)
picture_2 = torch.rand((1, 1401, 733, 3), dtype=torch.float32)
result = self.node.prepare(picture_1, picture_2, 1024, 576, 0.50, 0.50)
output_1, output_2 = result[0], result[1]
for output, source in ((output_1, picture_1), (output_2, picture_2)):
self.assertLessEqual(output.shape[1], source.shape[1])
self.assertLessEqual(output.shape[2], source.shape[2])
self.assertEqual(output.shape[1] % 32, 0)
self.assertEqual(output.shape[2] % 32, 0)
source_ratio = source.shape[2] / source.shape[1]
output_ratio = output.shape[2] / output.shape[1]
self.assertLess(abs(source_ratio - output_ratio) / source_ratio, 0.08)
self.assertEqual(output.dtype, source.dtype)
self.assertEqual(output.device, source.device)
self.assertLessEqual(
int(output_1.shape[1] * output_1.shape[2] + output_2.shape[1] * output_2.shape[2]),
int(1024 * 576 * 0.50),
)
def test_unused_budget_is_reassigned_and_an_aligned_small_image_is_not_upscaled(self) -> None:
picture_1 = torch.rand((1, 320, 320, 3), dtype=torch.float32)
picture_2 = torch.rand((1, 1024, 1024, 3), dtype=torch.float32)
result = self.node.prepare(picture_1, picture_2, 1024, 1024, 1.0, 0.80)
output_1, output_2 = result[0], result[1]
metadata = json.loads(result[2])
self.assertIs(output_1, picture_1)
self.assertEqual(tuple(output_1.shape[1:3]), (320, 320))
self.assertGreater(output_2.shape[1] * output_2.shape[2], int(1024 * 1024 * 0.20))
self.assertGreater(metadata["references"][1]["allocated_pixel_budget"], int(1024 * 1024 * 0.20))
def test_reference_one_share_changes_relative_output_budget(self) -> None:
picture_1 = torch.rand((1, 1024, 1024, 3), dtype=torch.float32)
picture_2 = torch.rand((1, 1024, 1024, 3), dtype=torch.float32)
result = self.node.prepare(picture_1, picture_2, 1024, 1024, 1.0, 0.70)
pixels_1 = result[3] * result[4]
pixels_2 = result[5] * result[6]
self.assertGreater(pixels_1, pixels_2)
self.assertLessEqual(pixels_1 + pixels_2, 1024 * 1024)
def test_prepared_pair_is_idempotent_for_the_same_budget(self) -> None:
picture_1 = torch.rand((1, 768, 768, 3), dtype=torch.float32)
picture_2 = torch.rand((1, 1808, 1024, 3), dtype=torch.float32)
first = self.node.prepare(picture_1, picture_2, 1376, 768, 1.0, 0.60)
second = self.node.prepare(first[0], first[1], 1376, 768, 1.0, 0.60)
self.assertEqual(tuple(first[0].shape), tuple(second[0].shape))
self.assertEqual(tuple(first[1].shape), tuple(second[1].shape))
self.assertIs(first[0], second[0])
self.assertIs(first[1], second[1])
def test_multi_image_batch_is_rejected_instead_of_silently_dropping_a_reference(self) -> None:
batch = torch.rand((2, 512, 512, 3), dtype=torch.float32)
singleton = torch.rand((1, 512, 512, 3), dtype=torch.float32)
with self.assertRaisesRegex(ValueError, "exactly one image"):
self.node.prepare(batch, singleton, 1024, 1024, 1.0, 0.50)
def test_malformed_or_non_rgb_images_are_rejected_clearly(self) -> None:
singleton = torch.rand((1, 512, 512, 3), dtype=torch.float32)
with self.assertRaisesRegex(ValueError, "shaped"):
self.node.prepare(torch.rand((512, 512, 3)), singleton, 1024, 1024, 1.0, 0.50)
with self.assertRaisesRegex(ValueError, "RGB"):
self.node.prepare(torch.rand((1, 512, 512, 4)), singleton, 1024, 1024, 1.0, 0.50)
if __name__ == "__main__":
unittest.main()