Files
exportAnything-ComfyUI-Diff…/tests/test_minimax_h3_fast_test_workflow.py
danrh 8f23c95546 Add synchronized music-video production planning
Add measured-audio, timed-lyrics, H3 lane planning, identity relay, upload-song routing, model-specific camera contracts, LTX long-horizon planning, workflow migrations, the validated V6 example, regression coverage, and an Unreleased changelog.
2026-08-23 08:03:39 -05:00

550 lines
19 KiB
Python

from __future__ import annotations
import copy
import importlib.util
import json
import tempfile
import unittest
import urllib.error
import urllib.request
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
MIGRATION_PATH = REPO_ROOT / "tools" / "migrate_minimax_h3_fast_test.py"
SPEC = importlib.util.spec_from_file_location("minimax_h3_fast_test", MIGRATION_PATH)
if SPEC is None or SPEC.loader is None:
raise RuntimeError(f"Cannot load migration module: {MIGRATION_PATH}")
MIGRATION = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(MIGRATION)
LIVE_CAPTURE = Path(
r"C:\Users\danrh\Desktop\14_minimax_ref2va+AUDIO_IN_PROCESS_SYNC_organized_LIVE_UNSAVED_CAPTURE_20260822_0057.json"
)
def _input(name: str, type_name: str) -> dict:
return {"name": name, "type": type_name, "link": None}
def _output(name: str, type_name: str) -> dict:
return {"name": name, "type": type_name, "links": []}
def _node(
node_id: int,
node_type: str,
*,
inputs: list[tuple[str, str]] = (),
outputs: list[tuple[str, str]] = (),
widgets: list | None = None,
pos: tuple[float, float] = (0.0, 0.0),
title: str | None = None,
) -> dict:
return {
"id": node_id,
"type": node_type,
"pos": list(pos),
"size": [300.0, 100.0],
"flags": {},
"order": node_id,
"mode": 0,
"inputs": [_input(name, type_name) for name, type_name in inputs],
"outputs": [_output(name, type_name) for name, type_name in outputs],
"title": title,
"properties": {"Node name for S&R": node_type},
"widgets_values": list(widgets or []),
}
def _main_connect(
workflow: dict,
link_id: int,
origin: dict,
output_name: str,
target: dict,
input_name: str,
link_type: str,
) -> None:
output_slot = next(
index for index, item in enumerate(origin["outputs"]) if item["name"] == output_name
)
input_slot = next(
index for index, item in enumerate(target["inputs"]) if item["name"] == input_name
)
workflow["links"].append(
[link_id, origin["id"], output_slot, target["id"], input_slot, link_type]
)
origin["outputs"][output_slot]["links"].append(link_id)
target["inputs"][input_slot]["link"] = link_id
def _sub_connect(
subgraph: dict,
link_id: int,
origin: dict,
output_name: str,
target: dict,
input_name: str,
link_type: str,
) -> None:
output_slot = next(
index for index, item in enumerate(origin["outputs"]) if item["name"] == output_name
)
input_slot = next(
index for index, item in enumerate(target["inputs"]) if item["name"] == input_name
)
subgraph["links"].append(
{
"id": link_id,
"origin_id": origin["id"],
"origin_slot": output_slot,
"target_id": target["id"],
"target_slot": input_slot,
"type": link_type,
}
)
origin["outputs"][output_slot]["links"].append(link_id)
target["inputs"][input_slot]["link"] = link_id
def _fixture() -> dict:
duration = _node(
178,
"PrimitiveFloat",
outputs=[("FLOAT", "FLOAT")],
widgets=[15.0],
title="Master duration",
)
splitter = _node(
187,
"DiffusionGemmaJSONSplitter",
widgets=["", 1.0, 32, "9:16 (Portrait Widescreen)"],
title="Resolution",
)
loader = _node(
661,
"UNETLoader",
outputs=[("MODEL", "MODEL")],
widgets=["minimax_h3_ref2va_int8_convrot.safetensors", "default"],
)
lora = _node(
713,
"LoraLoaderModelOnly",
inputs=[("model", "MODEL")],
outputs=[("MODEL", "MODEL")],
widgets=["minimax_h3_ref2v_turbo_4step_v0.1_bf16.safetensors", 1.0],
)
sage = _node(
715,
"PathchSageAttentionKJ",
inputs=[("model", "MODEL")],
outputs=[("MODEL", "MODEL")],
widgets=["auto", False],
)
cache = _node(
712,
"EasyCache",
inputs=[("model", "MODEL")],
outputs=[("MODEL", "MODEL")],
widgets=[0.2, 0.15, 0.95, True],
)
compile_node = _node(
714,
"TorchCompileModel",
inputs=[("model", "MODEL")],
outputs=[("MODEL", "MODEL")],
widgets=["inductor"],
)
outer = _node(
710,
"h3-fast-test-subgraph",
inputs=[("model", "MODEL")],
outputs=[("IMAGE", "IMAGE")],
)
project = _node(
676,
"DiffusionGemmaProjectMasterContract",
inputs=[("production_duration_seconds", "FLOAT")],
outputs=[("project_manifest_json", "STRING")],
widgets=[15.0, "9:16", 0.0, 15.0, "MiniMax H3 Ref2VA", "{}", 15.0],
)
target = _node(
673,
"DiffusionGemmaMiniMaxH3TargetProfile",
widgets=["ref2va", 0, "auto_scene_audio", "", "auto", 12, "off", 2, "", "auto", "", 0],
)
planner = _node(
677,
"DiffusionGemmaAudioAwareMultiShotPlanner",
inputs=[("project_manifest_json", "STRING")],
outputs=[("plan_json", "STRING")],
widgets=[0.0, 15.0, 12.0, 5.0, 15.0],
)
nodes = [
duration,
splitter,
loader,
lora,
sage,
cache,
compile_node,
outer,
project,
target,
planner,
]
workflow = {
"id": "fixture-workflow",
"revision": 12,
"last_node_id": 715,
"last_link_id": 1405,
"version": 0.4,
"nodes": nodes,
"links": [],
"groups": [],
"definitions": {"subgraphs": []},
"extra": {"fixture": {"native_shot_blocks": 4, "active_lanes": 1}},
}
_main_connect(workflow, 1398, loader, "MODEL", lora, "model", "MODEL")
_main_connect(workflow, 1404, lora, "MODEL", sage, "model", "MODEL")
_main_connect(workflow, 1405, sage, "MODEL", cache, "model", "MODEL")
_main_connect(workflow, 1402, cache, "MODEL", compile_node, "model", "MODEL")
_main_connect(workflow, 1403, compile_node, "MODEL", outer, "model", "MODEL")
_main_connect(
workflow, 1224, duration, "FLOAT", project, "production_duration_seconds", "FLOAT"
)
_main_connect(
workflow,
1244,
project,
"project_manifest_json",
planner,
"project_manifest_json",
"STRING",
)
sampler_select = _node(
657,
"KSamplerSelect",
outputs=[("SAMPLER", "SAMPLER")],
widgets=["res_multistep"],
)
scheduler = _node(
658,
"BasicScheduler",
inputs=[("model", "MODEL")],
outputs=[("SIGMAS", "SIGMAS")],
widgets=["simple", 8, 1.0],
)
h3_ids = [667, 684, 692, 700]
guider_ids = [660, 686, 694, 702]
sampler_ids = [659, 687, 695, 703]
sub_nodes = [sampler_select, scheduler]
h3_nodes = []
guiders = []
samplers = []
for lane, (h3_id, guider_id, sampler_id) in enumerate(
zip(h3_ids, guider_ids, sampler_ids)
):
y = float(lane * 970)
h3 = _node(
h3_id,
"MiniMaxH3ReferenceToVideo",
outputs=[("positive", "CONDITIONING"), ("LATENT", "LATENT")],
pos=(3748.0, y + 204.0),
)
guider = _node(
guider_id,
"BasicGuider",
inputs=[("model", "MODEL"), ("conditioning", "CONDITIONING")],
outputs=[("GUIDER", "GUIDER")],
pos=(4198.0, y + 354.0),
)
sampler = _node(
sampler_id,
"SamplerCustomAdvanced",
inputs=[
("noise", "NOISE"),
("guider", "GUIDER"),
("sampler", "SAMPLER"),
("sigmas", "SIGMAS"),
("latent_image", "LATENT"),
],
outputs=[("output", "LATENT")],
pos=(4618.0, y + 274.0),
)
h3_nodes.append(h3)
guiders.append(guider)
samplers.append(sampler)
sub_nodes.extend([h3, guider, sampler])
subgraph = {
"id": "h3-fast-test-subgraph",
"name": "New Subgraph",
"version": 1,
"revision": 12,
"state": {"lastNodeId": 715, "lastLinkId": 1405},
"inputNode": {"id": -10},
"outputNode": {"id": -20},
"inputs": [],
"outputs": [],
"nodes": sub_nodes,
"links": [],
"groups": [],
"widgets": [],
"config": {},
"extra": {},
}
link_id = 1259
for h3, guider, sampler in zip(h3_nodes, guiders, samplers):
_sub_connect(
subgraph, link_id, h3, "positive", guider, "conditioning", "CONDITIONING"
)
link_id += 1
_sub_connect(
subgraph, link_id, h3, "LATENT", sampler, "latent_image", "LATENT"
)
link_id += 1
workflow["definitions"]["subgraphs"].append(subgraph)
return workflow
def _node_by_id(workflow: dict, node_id: int) -> dict:
for node in workflow["nodes"]:
if int(node["id"]) == int(node_id):
return node
for subgraph in workflow["definitions"]["subgraphs"]:
for node in subgraph["nodes"]:
if int(node["id"]) == int(node_id):
return node
raise KeyError(node_id)
class FastTestMigrationUnitTests(unittest.TestCase):
def setUp(self) -> None:
self.source = _fixture()
self.source_before = copy.deepcopy(self.source)
self.migrated = MIGRATION.migrate_workflow(
self.source, source_file_sha256="a" * 64
)
def test_source_is_unchanged_and_migration_is_idempotent(self) -> None:
self.assertEqual(self.source, self.source_before)
MIGRATION.validate_workflow(
self.migrated, require_benchmark_defaults=True
)
self.assertEqual(
MIGRATION.migrate_workflow(self.migrated), self.migrated
)
def test_official_fast_asset_and_sampling_contract_is_active(self) -> None:
marker = self.migrated["extra"][MIGRATION.MIGRATION_SCHEMA]
loader = _node_by_id(self.migrated, marker["nodes"]["loader"])
lora = _node_by_id(self.migrated, marker["nodes"]["lora"])
sage = _node_by_id(self.migrated, marker["nodes"]["sage"])
sigma = _node_by_id(self.migrated, marker["nodes"]["sigma_shift"])
self.assertEqual(loader["widgets_values"][0], MIGRATION.FAST_MODEL)
self.assertEqual(lora["widgets_values"], [MIGRATION.FAST_LORA, 1.0])
self.assertEqual(sage["widgets_values"], ["auto", False])
self.assertEqual(
sigma["widgets_values"],
[MIGRATION.FAST_VIDEO_SHIFT, MIGRATION.FAST_AUDIO_SHIFT],
)
self.assertEqual(
marker["assets"]["turbo_lora"]["expected_sha256"],
MIGRATION.FAST_LORA_SHA256,
)
subgraph = self.migrated["definitions"]["subgraphs"][0]
sampler = next(
node for node in subgraph["nodes"] if node["type"] == "KSamplerSelect"
)
scheduler = next(
node for node in subgraph["nodes"] if node["type"] == "BasicScheduler"
)
self.assertEqual(sampler["widgets_values"], ["euler"])
self.assertEqual(scheduler["widgets_values"], ["simple", 4, 1.0])
def test_easycache_and_compile_are_disconnected_bypasses(self) -> None:
marker = self.migrated["extra"][MIGRATION.MIGRATION_SCHEMA]
disabled_ids = {
marker["nodes"]["disabled_easycache"],
marker["nodes"]["disabled_torch_compile"],
}
self.assertTrue(
all(_node_by_id(self.migrated, node_id)["mode"] == 4 for node_id in disabled_ids)
)
self.assertFalse(
any(
int(link[1]) in disabled_ids or int(link[3]) in disabled_ids
for link in self.migrated["links"]
)
)
def test_each_lane_cleans_conditioning_but_not_latent(self) -> None:
marker = self.migrated["extra"][MIGRATION.MIGRATION_SCHEMA]
self.assertEqual(len(marker["nodes"]["conditioning_cleanup"]), 4)
subgraph = self.migrated["definitions"]["subgraphs"][0]
links = {int(link["id"]): link for link in subgraph["links"]}
h3_nodes = sorted(
[node for node in subgraph["nodes"] if node["type"] == "MiniMaxH3ReferenceToVideo"],
key=lambda node: node["pos"][1],
)
guiders = sorted(
[node for node in subgraph["nodes"] if node["type"] == "BasicGuider"],
key=lambda node: node["pos"][1],
)
samplers = sorted(
[node for node in subgraph["nodes"] if node["type"] == "SamplerCustomAdvanced"],
key=lambda node: node["pos"][1],
)
for h3, cleanup_id, guider, sampler in zip(
h3_nodes,
marker["nodes"]["conditioning_cleanup"],
guiders,
samplers,
):
cleanup = _node_by_id(self.migrated, cleanup_id)
self.assertEqual(cleanup["type"], MIGRATION.CLEANUP_NODE_TYPE)
self.assertEqual(cleanup["inputs"][0]["name"], MIGRATION.CLEANUP_INPUT)
self.assertEqual(cleanup["outputs"][0]["name"], MIGRATION.CLEANUP_OUTPUT)
to_cleanup = links[int(cleanup["inputs"][0]["link"])]
to_guider = links[int(guider["inputs"][1]["link"])]
latent = links[int(sampler["inputs"][4]["link"])]
self.assertEqual(int(to_cleanup["origin_id"]), int(h3["id"]))
self.assertEqual(int(to_guider["origin_id"]), int(cleanup["id"]))
self.assertEqual(int(latent["origin_id"]), int(h3["id"]))
def test_fast_defaults_remain_configurable_after_creation(self) -> None:
self.assertEqual(_node_by_id(self.migrated, 178)["widgets_values"], [5.0])
self.assertEqual(_node_by_id(self.migrated, 187)["widgets_values"][1], 0.4)
edited = copy.deepcopy(self.migrated)
_node_by_id(edited, 178)["widgets_values"][0] = 12.0
_node_by_id(edited, 187)["widgets_values"][1] = 0.8
MIGRATION.validate_workflow(edited)
self.assertEqual(MIGRATION.migrate_workflow(edited), edited)
def test_project_native_shot_and_lane_contract_topology_is_preserved(self) -> None:
before_links = {
tuple(link)
for link in self.source["links"]
if int(link[1]) in {178, 676, 673, 677}
or int(link[3]) in {178, 676, 673, 677}
}
after_links = {
tuple(link)
for link in self.migrated["links"]
if int(link[1]) in {178, 676, 673, 677}
or int(link[3]) in {178, 676, 673, 677}
}
self.assertEqual(before_links, after_links)
self.assertEqual(
self.migrated["extra"]["fixture"],
{"native_shot_blocks": 4, "active_lanes": 1},
)
semantics = self.migrated["extra"][MIGRATION.MIGRATION_SCHEMA][
"generation_semantics"
]
self.assertEqual(semantics["lazy_generation_lane_capacity"], 4)
self.assertTrue(semantics["project_and_native_shot_contract_preserved"])
self.assertEqual(
semantics["cleanup_node_type"], MIGRATION.CLEANUP_NODE_TYPE
)
self.assertFalse(semantics["cleanup_backend_output_node"])
def test_changed_source_chain_fails_closed(self) -> None:
broken = _fixture()
outer = _node_by_id(broken, 710)
link_id = outer["inputs"][0]["link"]
link = next(link for link in broken["links"] if int(link[0]) == int(link_id))
link[1] = 715
with self.assertRaises(MIGRATION.WorkflowError):
MIGRATION.migrate_workflow(broken, source_file_sha256="b" * 64)
def test_cleanup_or_latent_contract_break_is_rejected(self) -> None:
broken = copy.deepcopy(self.migrated)
marker = broken["extra"][MIGRATION.MIGRATION_SCHEMA]
cleanup = _node_by_id(broken, marker["nodes"]["conditioning_cleanup"][0])
cleanup["outputs"][0]["links"] = []
with self.assertRaises(MIGRATION.WorkflowError):
MIGRATION.validate_workflow(broken)
def test_file_migration_creates_only_a_new_sibling_and_is_repeatable(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
source = root / "source.json"
output = root / "source_FAST.json"
source_bytes = json.dumps(self.source, indent=2).encode("utf-8") + b"\n"
source.write_bytes(source_bytes)
source_hash = MIGRATION._file_hash(source_bytes)
migrated, observed_hash, written = MIGRATION.migrate_file(
source,
output_path=output,
write=True,
expected_source_sha256=source_hash,
)
self.assertEqual(observed_hash, source_hash)
self.assertEqual(written, output.resolve())
self.assertEqual(source.read_bytes(), source_bytes)
self.assertTrue(output.is_file())
self.assertEqual(json.loads(output.read_text(encoding="utf-8")), migrated)
_same, _hash, second_written = MIGRATION.migrate_file(
source,
output_path=output,
write=True,
expected_source_sha256=source_hash,
)
self.assertIsNone(second_written)
self.assertEqual(source.read_bytes(), source_bytes)
def test_output_may_never_equal_source(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
source = Path(temporary) / "source.json"
source.write_text(json.dumps(self.source), encoding="utf-8")
with self.assertRaises(MIGRATION.WorkflowError):
MIGRATION.migrate_file(
source,
output_path=source,
write=True,
expected_source_sha256=MIGRATION._file_hash(source.read_bytes()),
)
@unittest.skipUnless(LIVE_CAPTURE.is_file(), "exact live capture is absent")
class ExactLiveCaptureIntegrationTests(unittest.TestCase):
def test_exact_captured_unsaved_canvas_migrates_without_writing(self) -> None:
migrated, source_hash, written = MIGRATION.migrate_file(
LIVE_CAPTURE,
output_path=MIGRATION.DEFAULT_OUTPUT,
write=False,
expected_source_sha256=MIGRATION.SOURCE_CAPTURE_SHA256,
)
self.assertEqual(source_hash, MIGRATION.SOURCE_CAPTURE_SHA256)
self.assertIsNone(written)
MIGRATION.validate_workflow(migrated, require_benchmark_defaults=True)
marker = migrated["extra"][MIGRATION.MIGRATION_SCHEMA]
self.assertEqual(marker["source"]["file_sha256"], source_hash)
self.assertEqual(
marker["generation_semantics"]["lazy_generation_lane_capacity"], 4
)
class InstalledCleanupBackendContractTests(unittest.TestCase):
def test_live_backend_reports_cleanup_passthrough_is_not_an_output_node(self) -> None:
try:
with urllib.request.urlopen(
"http://127.0.0.1:8188/object_info/FL_UnloadAllModels",
timeout=2.0,
) as response:
payload = json.load(response)
except (OSError, urllib.error.URLError) as exc:
self.skipTest(f"local ComfyUI object_info is unavailable: {exc}")
contract = payload["FL_UnloadAllModels"]
self.assertFalse(contract["output_node"])
self.assertEqual(contract["input_order"]["required"], ["value"])
self.assertEqual(contract["output_name"], ["*"])
self.assertEqual(contract["output"], ["*"])
if __name__ == "__main__":
unittest.main()