Files
exportAnything-ComfyUI-Diff…/tests/test_director_cache.py
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

501 lines
19 KiB
Python

from __future__ import annotations
import asyncio
import copy
import importlib.util
import json
import math
import os
import sys
import tempfile
import unittest
import uuid
from pathlib import Path
from unittest.mock import patch
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_director_cache_{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 DirectorCacheTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.nodes = load_nodes_module()
def runtime(self, *, unload_policy: str = "keep_loaded", model_path: str = "unused"):
return self.nodes.RuntimeConfig(
model_path=model_path,
backend="transformers_inprocess",
dtype="auto",
quantization="modelopt_nvfp4",
local_files_only=True,
unload_policy=unload_policy,
max_memory_gb=20.0,
status={"ready": True, "supports_pixels": True},
)
def context(self, images=None):
return self.nodes.GemmaContext(
user_prompt="A short verified scene.",
images=images,
source="image" if images is not None else "none",
media_metadata={
"source": "image" if images is not None else "none",
"reference_image_count": 1 if images is not None else 0,
},
)
def target(self):
return self.nodes.TargetProfileConfig(target_profile="ltx")
@staticmethod
def successful_result():
report = {
"schema": "dg-grounding-report/1",
"config": {"mode": "strict"},
"analysis_status": "not_applicable",
"decision": "not_applicable",
"would_block": False,
"grounding_guard_would_block": False,
"blocked_reasons": [],
"attempt_count": 0,
"retry_reasons": [],
"model_call_count": 1,
}
metadata = {
"ready_for_generation": True,
"json_parse_valid": True,
"used_template_fallback": False,
"salvage_warning": "",
"grounding_guard": report,
}
packet = {
"ltx_prompt": "A complete verified local video prompt.",
"ideogram_prompt": "",
"minimax_h3_prompt": "",
"negative_prompt": "",
"scene_segments": [],
"metadata": metadata,
}
return packet, "raw", "reasoning", "", True, "", metadata
def generate(self, node, runtime, *, guard=None, cache_mode="reuse"):
return node.generate(
runtime,
self.context(),
self.target(),
temperature=0.45,
creativity_mode="editorial",
creative_strength=0.6,
thinking_mode="off",
max_new_tokens=2048,
director_cache_mode=cache_mode,
grounding_guard_config=guard or {"mode": "strict"},
unique_id="node-does-not-affect-disk-key",
)
def test_second_call_hits_disk_without_running_director_again(self) -> None:
fixed_key = "a" * 64
with tempfile.TemporaryDirectory() as temporary, patch.dict(
os.environ,
{"DG_DIRECTOR_CACHE_DIR": temporary},
), patch.object(
self.nodes,
"_director_cache_key",
return_value=fixed_key,
), patch.object(
self.nodes,
"_run_generation_packet",
side_effect=lambda *_args, **_kwargs: copy.deepcopy(self.successful_result()),
) as director:
node = self.nodes.DiffusionGemmaCoTGenerator()
first = self.generate(node, self.runtime())
second = self.generate(node, self.runtime())
self.assertEqual(director.call_count, 1)
self.assertEqual(json.loads(first[3])["director_cache"]["status"], "stored")
second_metadata = json.loads(second[3])
self.assertTrue(second_metadata["director_cache"]["hit"])
self.assertTrue(second_metadata["director_runtime"]["cache_hit"])
self.assertEqual(second_metadata["director_runtime"]["generation_seconds"], 0.0)
self.assertEqual(second[1], "reasoning")
self.assertEqual(second[2], "raw")
def test_cache_hit_honors_unload_after_run(self) -> None:
fixed_key = "b" * 64
with tempfile.TemporaryDirectory() as temporary, patch.dict(
os.environ,
{"DG_DIRECTOR_CACHE_DIR": temporary},
), patch.object(
self.nodes,
"_director_cache_key",
return_value=fixed_key,
), patch.object(
self.nodes,
"_run_generation_packet",
side_effect=lambda *_args, **_kwargs: copy.deepcopy(self.successful_result()),
), patch.object(self.nodes, "_release_transformers_runtime") as release:
node = self.nodes.DiffusionGemmaCoTGenerator()
runtime = self.runtime(unload_policy="unload_after_run")
self.generate(node, runtime)
self.generate(node, runtime)
# The mocked first Director call does not own the real wrapper finally;
# the second, cached call explicitly enforces the lifecycle policy.
release.assert_called_once_with()
def test_corrupt_entry_causes_safe_recomputation(self) -> None:
fixed_key = "c" * 64
with tempfile.TemporaryDirectory() as temporary, patch.dict(
os.environ,
{"DG_DIRECTOR_CACHE_DIR": temporary},
), patch.object(
self.nodes,
"_director_cache_key",
return_value=fixed_key,
), patch.object(
self.nodes,
"_run_generation_packet",
side_effect=lambda *_args, **_kwargs: copy.deepcopy(self.successful_result()),
) as director:
node = self.nodes.DiffusionGemmaCoTGenerator()
self.generate(node, self.runtime())
self.nodes._director_disk_cache_path(fixed_key).write_text("{", encoding="utf-8")
result = self.generate(node, self.runtime())
self.assertEqual(director.call_count, 2)
self.assertFalse(json.loads(result[3])["director_cache"]["hit"])
def test_detailed_trace_bypasses_cache_side_effects(self) -> None:
with tempfile.TemporaryDirectory() as temporary, patch.dict(
os.environ,
{"DG_DIRECTOR_CACHE_DIR": temporary},
), patch.object(
self.nodes,
"_run_generation_packet",
side_effect=lambda *_args, **_kwargs: copy.deepcopy(self.successful_result()),
) as director:
node = self.nodes.DiffusionGemmaCoTGenerator()
guard = {"mode": "strict", "save_detailed_trace": True}
first = self.generate(node, self.runtime(), guard=guard)
second = self.generate(node, self.runtime(), guard=guard)
self.assertEqual(director.call_count, 2)
self.assertEqual(
json.loads(first[3])["director_cache"]["status"],
"bypassed_for_detailed_trace",
)
self.assertEqual(
json.loads(second[3])["director_cache"]["status"],
"bypassed_for_detailed_trace",
)
self.assertEqual(list(Path(temporary).glob("**/*.json")), [])
def test_reuse_is_stable_while_explicit_rerun_modes_bypass_native_cache(self) -> None:
first = self.nodes.DiffusionGemmaCoTGenerator.IS_CHANGED(
director_cache_mode="reuse"
)
second = self.nodes.DiffusionGemmaCoTGenerator.IS_CHANGED(
director_cache_mode="reuse"
)
self.assertEqual(first, second)
self.assertEqual(
first,
f"director-reuse:{self.nodes.DIRECTOR_EXECUTION_CONTRACT_REVISION}",
)
for mode in ("refresh", "off"):
with self.subTest(director_cache_mode=mode):
value = self.nodes.DiffusionGemmaCoTGenerator.IS_CHANGED(
director_cache_mode=mode
)
self.assertTrue(math.isnan(value))
def test_detailed_trace_guard_bypasses_native_cache(self) -> None:
stable = self.nodes.DiffusionGemmaGroundingGuardSettings.IS_CHANGED(
save_detailed_trace=False
)
self.assertEqual(stable, "grounding-guard-config-v1")
changed = self.nodes.DiffusionGemmaGroundingGuardSettings.IS_CHANGED(
save_detailed_trace=True
)
self.assertTrue(math.isnan(changed))
linked = self.nodes.DiffusionGemmaGroundingGuardSettings.IS_CHANGED(
save_detailed_trace=None
)
self.assertTrue(math.isnan(linked))
def test_comfy_signature_cache_isolates_an_ltx_only_edit_from_h3(self) -> None:
# Other test modules install lightweight Comfy stubs during discovery.
# Temporarily replace that module family with the real local ComfyUI
# implementation, then restore the discovery-time modules afterward.
module_prefixes = (
"comfy",
"comfy_api",
"comfy_execution",
"execution",
"nodes",
)
saved_modules = {
name: module
for name, module in tuple(sys.modules.items())
if any(name == prefix or name.startswith(f"{prefix}.") for prefix in module_prefixes)
}
for name in saved_modules:
sys.modules.pop(name, None)
def restore_modules() -> None:
for name in tuple(sys.modules):
if any(
name == prefix or name.startswith(f"{prefix}.")
for prefix in module_prefixes
):
sys.modules.pop(name, None)
sys.modules.update(saved_modules)
self.addCleanup(restore_modules)
import nodes as comfy_nodes
from comfy_execution.caching import CacheKeySetInputSignature, LRUCache
from comfy_execution.graph import DynamicPrompt
from execution import CacheEntry, IsChangedCache
class StableSource:
@classmethod
def INPUT_TYPES(cls):
return {"required": {"value": ("STRING",)}}
RETURN_TYPES = ("STRING",)
FUNCTION = "run"
class StablePass:
@classmethod
def INPUT_TYPES(cls):
return {"required": {"source": ("STRING",)}}
RETURN_TYPES = ("STRING",)
FUNCTION = "run"
class StableMerge:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"left": ("STRING",),
"right": ("STRING",),
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "run"
class StableOutput(StablePass):
OUTPUT_NODE = True
class_types = {
"DGCacheTestSource": StableSource,
"DGCacheTestPass": StablePass,
"DGCacheTestMerge": StableMerge,
"DGCacheTestOutput": StableOutput,
"DiffusionGemmaCoTGenerator": self.nodes.DiffusionGemmaCoTGenerator,
"DiffusionGemmaGroundingGuardSettings": (
self.nodes.DiffusionGemmaGroundingGuardSettings
),
}
base_prompt = {
"shared": {
"class_type": "DGCacheTestSource",
"inputs": {"value": "shared creative brief"},
},
"510": {
"class_type": "DGCacheTestSource",
"inputs": {"value": "LTX profile A"},
},
"186": {
"class_type": "DGCacheTestMerge",
"inputs": {
"left": ["shared", 0],
"right": ["510", 0],
},
},
"ltx_output": {
"class_type": "DGCacheTestOutput",
"inputs": {"source": ["186", 0]},
},
"182": {
"class_type": "DGCacheTestSource",
"inputs": {"value": "model config"},
},
"513": {
"class_type": "DGCacheTestPass",
"inputs": {"source": ["shared", 0]},
},
"515": {
"class_type": "DiffusionGemmaGroundingGuardSettings",
"inputs": {"save_detailed_trace": False},
},
"518": {
"class_type": "DGCacheTestSource",
"inputs": {"value": "H3 target profile"},
},
"516": {
"class_type": "DiffusionGemmaCoTGenerator",
"inputs": {
"model_config": ["182", 0],
"gemma_context": ["513", 0],
"target_profile_config": ["518", 0],
"grounding_guard_config": ["515", 0],
"temperature": 0.45,
"creativity_mode": "wild",
"creative_strength": 0.2,
"thinking_mode": "off",
"max_new_tokens": 2048,
"director_cache_mode": "reuse",
},
},
"517": {
"class_type": "DGCacheTestPass",
"inputs": {"source": ["516", 0]},
},
"h3_output": {
"class_type": "DGCacheTestOutput",
"inputs": {"source": ["517", 0]},
},
}
async def cache_hits_after(changed_prompt):
cache = LRUCache(CacheKeySetInputSignature, max_size=1)
first = DynamicPrompt(copy.deepcopy(base_prompt))
await cache.set_prompt(
first,
base_prompt.keys(),
IsChangedCache("first", first, cache),
)
cache.clean_unused()
for node_id in base_prompt:
cache.set_local(node_id, CacheEntry(ui=None, outputs=[node_id]))
second = DynamicPrompt(changed_prompt)
await cache.set_prompt(
second,
changed_prompt.keys(),
IsChangedCache("second", second, cache),
)
cache.clean_unused()
return {
node_id: cache.get_local(node_id) is not None
for node_id in changed_prompt
}
with patch.dict(comfy_nodes.NODE_CLASS_MAPPINGS, class_types):
ltx_edit = copy.deepcopy(base_prompt)
ltx_edit["510"]["inputs"]["value"] = "LTX profile B"
ltx_hits = asyncio.run(cache_hits_after(ltx_edit))
self.assertFalse(ltx_hits["510"])
self.assertFalse(ltx_hits["186"])
self.assertFalse(ltx_hits["ltx_output"])
for node_id in ("shared", "513", "515", "516", "517", "h3_output"):
self.assertTrue(ltx_hits[node_id], node_id)
shared_edit = copy.deepcopy(base_prompt)
shared_edit["shared"]["inputs"]["value"] = "changed shared brief"
shared_hits = asyncio.run(cache_hits_after(shared_edit))
for node_id in ("shared", "513", "516", "517", "h3_output"):
self.assertFalse(shared_hits[node_id], node_id)
trace_edit = copy.deepcopy(base_prompt)
trace_edit["515"]["inputs"]["save_detailed_trace"] = True
trace_hits = asyncio.run(cache_hits_after(trace_edit))
for node_id in ("515", "516", "517", "h3_output"):
self.assertFalse(trace_hits[node_id], node_id)
refresh_edit = copy.deepcopy(base_prompt)
refresh_edit["516"]["inputs"]["director_cache_mode"] = "refresh"
refresh_hits = asyncio.run(cache_hits_after(refresh_edit))
for node_id in ("516", "517", "h3_output"):
self.assertFalse(refresh_hits[node_id], node_id)
def test_cache_key_tracks_pixels_and_semantics_but_not_unload_policy(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
checkpoint = Path(temporary) / "model.gguf"
checkpoint.write_bytes(b"small-test-checkpoint")
images = torch.zeros((1, 2, 2, 3), dtype=torch.float32)
context = self.context(images)
target = self.target()
common = dict(
temperature=0.45,
creativity_mode="editorial",
creative_strength=0.6,
thinking_mode="off",
max_new_tokens=2048,
)
key = self.nodes._director_cache_key(
self.runtime(model_path=str(checkpoint)),
context,
target,
{"mode": "strict", "seed": 7},
**common,
)
unload_key = self.nodes._director_cache_key(
self.runtime(
unload_policy="unload_after_run",
model_path=str(checkpoint),
),
context,
target,
{"mode": "strict", "seed": 7},
**common,
)
changed_images = images.clone()
changed_images[0, 0, 0, 0] = 1.0
pixel_key = self.nodes._director_cache_key(
self.runtime(model_path=str(checkpoint)),
self.context(changed_images),
target,
{"mode": "strict", "seed": 7},
**common,
)
duration_target = self.nodes.TargetProfileConfig(
target_profile="ltx",
target_duration_seconds=15.0,
)
target_key = self.nodes._director_cache_key(
self.runtime(model_path=str(checkpoint)),
context,
duration_target,
{"mode": "strict", "seed": 7},
**common,
)
self.assertEqual(key, unload_key)
self.assertNotEqual(key, pixel_key)
self.assertNotEqual(key, target_key)
def test_rank_three_tensor_hashes_as_one_image(self) -> None:
rank_three = torch.zeros((2, 3, 3), dtype=torch.float32)
rank_four = torch.zeros((2, 2, 3, 3), dtype=torch.float32)
self.assertEqual(len(self.nodes._ordered_tensor_content_hashes(rank_three)), 1)
self.assertEqual(len(self.nodes._ordered_tensor_content_hashes(rank_four)), 2)
if __name__ == "__main__":
unittest.main()