Prepare DiffusionGemma Director 0.2.0

This commit is contained in:
danrh
2026-08-07 15:11:28 -05:00
parent 119634ca54
commit dfcd642a21
27 changed files with 29785 additions and 108 deletions
+88
View File
@@ -0,0 +1,88 @@
from __future__ import annotations
import sys
from pathlib import Path
from types import ModuleType, SimpleNamespace
import wave
import torch
import torch.nn.functional as torch_functional
def install_comfy_stubs() -> None:
"""Install the narrow ComfyUI seams exercised by the reframe unit tests."""
folder_paths = ModuleType("folder_paths")
folder_paths.get_input_directory = lambda: str(Path.cwd())
folder_paths.annotated_filepath = lambda name: (name, None)
folder_paths.filter_files_content_types = lambda files, _types: list(files)
def load_audio(path: str):
with wave.open(str(path), "rb") as handle:
if handle.getsampwidth() != 2:
raise ValueError("test audio stub supports 16-bit PCM only")
channels = handle.getnchannels()
frame_count = handle.getnframes()
sample_rate = handle.getframerate()
payload = bytearray(handle.readframes(frame_count))
samples = torch.frombuffer(payload, dtype=torch.int16).reshape(-1, channels)
waveform = samples.transpose(0, 1).to(torch.float32).div_(32768.0).contiguous()
return waveform, sample_rate
comfy_extras = ModuleType("comfy_extras")
comfy_extras.__path__ = []
comfy_audio = ModuleType("comfy_extras.nodes_audio")
comfy_audio.load = load_audio
comfy_extras.nodes_audio = comfy_audio
comfy = ModuleType("comfy")
comfy.__path__ = []
comfy_utils = ModuleType("comfy.utils")
comfy_utils.common_upscale = lambda images, width, height, _method, _crop: (
torch_functional.interpolate(
images,
size=(int(height), int(width)),
mode="bilinear",
align_corners=False,
)
)
comfy.utils = comfy_utils
class UnsupportedVideo:
def __init__(self, *_args, **_kwargs):
raise RuntimeError("video decoding is outside this unit-test seam")
comfy_latest = ModuleType("comfy_api.latest")
comfy_latest.InputImpl = SimpleNamespace(
VideoFromFile=UnsupportedVideo,
VideoFromComponents=UnsupportedVideo,
)
comfy_latest.Types = SimpleNamespace(
VideoComponents=lambda **values: SimpleNamespace(**values)
)
comfy_api = ModuleType("comfy_api")
comfy_api.__path__ = []
comfy_api.latest = comfy_latest
aiohttp_web = ModuleType("aiohttp.web")
aiohttp_web.json_response = lambda data, status=200: SimpleNamespace(
data=data,
status=status,
)
aiohttp = ModuleType("aiohttp")
aiohttp.__path__ = []
aiohttp.web = aiohttp_web
sys.modules.update(
{
"aiohttp": aiohttp,
"aiohttp.web": aiohttp_web,
"comfy": comfy,
"comfy.utils": comfy_utils,
"comfy_api": comfy_api,
"comfy_api.latest": comfy_latest,
"comfy_extras": comfy_extras,
"comfy_extras.nodes_audio": comfy_audio,
"folder_paths": folder_paths,
}
)
+365
View File
@@ -0,0 +1,365 @@
from __future__ import annotations
import json
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
import wave
import torch
from comfy_test_stubs import install_comfy_stubs
install_comfy_stubs()
from ltx_reframe import (
LTXReframeCustomAudioLoader,
LTXReframeRectilinearCorrection,
LTXReframeTargetFit,
NODE_CLASS_MAPPINGS,
build_reframe_masks,
compute_reframe_placement,
ensure_audio_track,
fit_audio_track,
prepare_reframe_canvas,
rectilinear_correct_images,
resolve_frame_range,
snap_dimension,
)
class LTXReframeTests(unittest.TestCase):
@staticmethod
def _write_test_wav(path: Path, *, sample_rate: int = 8000, samples: int = 80):
with wave.open(str(path), "wb") as handle:
handle.setnchannels(1)
handle.setsampwidth(2)
handle.setframerate(sample_rate)
handle.writeframes(b"\x00\x00" * samples)
def test_custom_audio_loader_is_registered_and_accepts_an_empty_lazy_branch(self):
self.assertIs(
NODE_CLASS_MAPPINGS["LTXReframeCustomAudioLoader"],
LTXReframeCustomAudioLoader,
)
self.assertTrue(LTXReframeCustomAudioLoader.VALIDATE_INPUTS(""))
with self.assertRaisesRegex(ValueError, "no custom audio file is selected"):
LTXReframeCustomAudioLoader().load_audio("")
def test_custom_audio_loader_lists_audio_uploads_with_a_blank_default(self):
with tempfile.TemporaryDirectory() as directory:
with (
patch("ltx_reframe.folder_paths.get_input_directory", return_value=directory),
patch(
"ltx_reframe.folder_paths.filter_files_content_types",
return_value=["song.wav"],
),
):
spec = LTXReframeCustomAudioLoader.INPUT_TYPES()
options, config = spec["required"]["audio_file"]
self.assertEqual(options, ["", "song.wav"])
self.assertTrue(config["audio_upload"])
def test_custom_audio_loader_decodes_and_fingerprints_an_input_file(self):
with tempfile.TemporaryDirectory() as directory:
audio_path = Path(directory) / "clip.wav"
self._write_test_wav(audio_path)
with (
patch("ltx_reframe.folder_paths.get_input_directory", return_value=directory),
patch(
"ltx_reframe.folder_paths.annotated_filepath",
side_effect=lambda name: (name, None),
),
):
audio, = LTXReframeCustomAudioLoader().load_audio("clip.wav")
first_fingerprint = LTXReframeCustomAudioLoader.IS_CHANGED("clip.wav")
second_fingerprint = LTXReframeCustomAudioLoader.IS_CHANGED("clip.wav")
with audio_path.open("ab") as handle:
handle.write(b"changed")
changed_fingerprint = LTXReframeCustomAudioLoader.IS_CHANGED("clip.wav")
self.assertEqual(audio["sample_rate"], 8000)
self.assertEqual(tuple(audio["waveform"].shape), (1, 1, 80))
self.assertEqual(first_fingerprint, second_fingerprint)
self.assertNotEqual(first_fingerprint, changed_fingerprint)
self.assertEqual(len(first_fingerprint), 64)
def test_custom_audio_loader_reports_a_missing_input_file(self):
with tempfile.TemporaryDirectory() as directory:
with (
patch("ltx_reframe.folder_paths.get_input_directory", return_value=directory),
patch(
"ltx_reframe.folder_paths.annotated_filepath",
side_effect=lambda name: (name, None),
),
):
with self.assertRaisesRegex(ValueError, "file was not found"):
LTXReframeCustomAudioLoader().load_audio("missing.wav")
def test_custom_audio_loader_rejects_paths_outside_comfyui_input(self):
with tempfile.TemporaryDirectory() as directory:
with (
patch("ltx_reframe.folder_paths.get_input_directory", return_value=directory),
patch(
"ltx_reframe.folder_paths.annotated_filepath",
return_value=("../outside.wav", None),
),
):
with self.assertRaisesRegex(ValueError, "input directory"):
LTXReframeCustomAudioLoader().load_audio("../outside.wav")
def test_custom_audio_example_workflow_has_consistent_links_and_safe_defaults(self):
workflow_path = (
Path(__file__).resolve().parents[1]
/ "examples"
/ "gemmaREFRAME_Experimental_audio_on-off.json"
)
workflow = json.loads(workflow_path.read_text(encoding="utf-8"))
nodes = {node["id"]: node for node in workflow["nodes"]}
links = {link[0]: link for link in workflow["links"]}
self.assertEqual(len(nodes), len(workflow["nodes"]))
self.assertEqual(len(links), len(workflow["links"]))
for link_id, (_, source_id, source_slot, target_id, target_slot, _type) in links.items():
self.assertIn(source_id, nodes)
self.assertIn(target_id, nodes)
self.assertIn(link_id, nodes[source_id]["outputs"][source_slot].get("links") or [])
self.assertEqual(nodes[target_id]["inputs"][target_slot].get("link"), link_id)
custom_loader = nodes[9086]
selector = nodes[9112]
reframe = nodes[5553]
stage_one = nodes[9055]
create_video = nodes[5539]
preview = nodes[9088]
self.assertEqual(custom_loader["type"], "LTXReframeCustomAudioLoader")
self.assertEqual(custom_loader["widgets_values"], [""])
self.assertEqual(selector["type"], "LazySwitchKJ")
self.assertFalse(selector["widgets_values"][0])
self.assertEqual(set(selector["outputs"][0]["links"]), {14666, 14818, 14918})
self.assertEqual(reframe["outputs"][4]["links"], [14990, 14991])
self.assertEqual(stage_one["inputs"][10]["link"], 14818)
self.assertEqual(preview["inputs"][0]["link"], 14918)
self.assertEqual(create_video["inputs"][1]["link"], 14999)
self.assertEqual(create_video["inputs"][2]["link"], 14666)
def test_existing_audio_is_preserved(self):
audio = {
"waveform": torch.ones((1, 1, 100), dtype=torch.float32),
"sample_rate": 16000,
}
result, synthesized = ensure_audio_track(audio, frame_count=24, fps=24.0)
self.assertIs(result, audio)
self.assertFalse(synthesized)
def test_audio_less_video_gets_duration_matched_stereo_silence(self):
audio, synthesized = ensure_audio_track(None, frame_count=121, fps=24.0)
self.assertTrue(synthesized)
self.assertEqual(audio["sample_rate"], 44100)
self.assertEqual(tuple(audio["waveform"].shape), (1, 2, 222338))
self.assertEqual(torch.count_nonzero(audio["waveform"]).item(), 0)
def test_silent_audio_fallback_rejects_invalid_fps(self):
with self.assertRaisesRegex(ValueError, "valid video frame rate"):
ensure_audio_track(None, frame_count=121, fps=0.0)
def test_audio_is_cropped_and_padded_to_selected_duration(self):
cropped, synthesized = fit_audio_track(
{"waveform": torch.ones((1, 2, 2000)), "sample_rate": 1000},
frame_count=25,
fps=25.0,
)
padded, _ = fit_audio_track(
{"waveform": torch.ones((1, 2, 500)), "sample_rate": 1000},
frame_count=25,
fps=25.0,
)
self.assertFalse(synthesized)
self.assertEqual(cropped["waveform"].shape[-1], 1000)
self.assertEqual(padded["waveform"].shape[-1], 1000)
self.assertEqual(torch.count_nonzero(padded["waveform"][..., 500:]).item(), 0)
def test_full_video_range_is_not_aligned_or_trimmed(self):
selected = resolve_frame_range(70, 25.0, False, 8, 16)
self.assertFalse(selected.range_enabled)
self.assertEqual(selected.effective_first_frame, 0)
self.assertEqual(selected.effective_last_frame, 69)
self.assertEqual(selected.selected_frame_count, 70)
def test_valid_sixty_five_frame_range_is_preserved(self):
selected = resolve_frame_range(241, 25.0, True, 0, 64)
self.assertEqual(selected.effective_first_frame, 0)
self.assertEqual(selected.effective_last_frame, 64)
self.assertEqual(selected.selected_frame_count, 65)
def test_range_alignment_preserves_first_frame_and_trims_end(self):
zero_based = resolve_frame_range(241, 25.0, True, 0, 70)
offset = resolve_frame_range(241, 25.0, True, 100, 199)
self.assertEqual((zero_based.effective_first_frame, zero_based.effective_last_frame), (0, 64))
self.assertEqual(zero_based.selected_frame_count, 65)
self.assertEqual((offset.effective_first_frame, offset.effective_last_frame), (100, 196))
self.assertEqual(offset.selected_frame_count, 97)
def test_last_frame_minus_one_resolves_to_source_end_then_aligns(self):
selected = resolve_frame_range(100, 25.0, True, 10, -1)
self.assertEqual(selected.requested_last_frame, 99)
self.assertEqual(selected.effective_last_frame, 98)
self.assertEqual(selected.selected_frame_count, 89)
def test_invalid_frame_ranges_are_rejected(self):
with self.assertRaisesRegex(ValueError, "greater than or equal"):
resolve_frame_range(100, 25.0, True, 30, 20)
with self.assertRaisesRegex(ValueError, "outside"):
resolve_frame_range(100, 25.0, True, 0, 100)
with self.assertRaisesRegex(ValueError, "at least nine"):
resolve_frame_range(100, 25.0, True, 0, 7)
def test_single_frame_range_is_allowed(self):
selected = resolve_frame_range(100, 25.0, True, 42, 42)
self.assertEqual(selected.selected_frame_count, 1)
self.assertEqual(selected.effective_last_frame, 42)
def test_dimensions_snap_to_ltx_multiple(self):
self.assertEqual(snap_dimension(1919), 1920)
self.assertEqual(snap_dimension(1050), 1024)
self.assertEqual(snap_dimension(1), 256)
def test_portrait_canvas_centers_aspect_locked_landscape_source(self):
placement = compute_reframe_placement(1920, 1080, 1088, 1920, 0.5, 0.5, 0.7)
self.assertEqual(placement.target_width, 1088)
self.assertEqual(placement.target_height, 1920)
self.assertLess(abs(placement.box_width / placement.box_height - 16 / 9), 0.01)
self.assertLessEqual(abs(placement.box_x * 2 + placement.box_width - placement.target_width), 1)
self.assertLessEqual(abs(placement.box_y * 2 + placement.box_height - placement.target_height), 1)
def test_canvas_and_masks_preserve_source_region(self):
frames = torch.ones((2, 8, 16, 3), dtype=torch.float32)
placement = compute_reframe_placement(16, 8, 256, 256, 0.5, 0.5, 0.5)
canvas = prepare_reframe_canvas(frames, placement)
outpaint, source = build_reframe_masks(placement, feather=0)
self.assertEqual(tuple(canvas.shape), (2, 256, 256, 3))
self.assertEqual(tuple(outpaint.shape), (1, 256, 256))
self.assertEqual(tuple(source.shape), (1, 256, 256))
self.assertTrue(torch.allclose(outpaint + source, torch.ones_like(source)))
self.assertEqual(canvas[:, placement.box_y, placement.box_x].mean().item(), 1.0)
self.assertEqual(canvas[:, 0, 0].sum().item(), 0.0)
def test_feather_ramps_only_outside_source_box(self):
placement = compute_reframe_placement(16, 9, 256, 256, 0.5, 0.5, 0.5)
outpaint, source = build_reframe_masks(placement, feather=16)
inside_y = placement.box_y + placement.box_height // 2
inside_x = placement.box_x + placement.box_width // 2
self.assertEqual(outpaint[0, inside_y, inside_x].item(), 0.0)
self.assertEqual(source[0, inside_y, inside_x].item(), 1.0)
self.assertEqual(outpaint[0, 0, 0].item(), 1.0)
def test_zoom_in_can_extend_source_beyond_target(self):
placement = compute_reframe_placement(1920, 1080, 1088, 1920, 0.5, 0.5, 2.0)
self.assertEqual(placement.box_width, 2176)
self.assertEqual(placement.box_height, 1224)
self.assertEqual(placement.box_x, -544)
self.assertEqual(placement.box_y, 348)
self.assertEqual(placement.source_scale, 2.0)
def test_zoomed_source_is_cropped_safely_to_target_canvas(self):
frames = torch.ones((2, 8, 16, 3), dtype=torch.float32)
placement = compute_reframe_placement(16, 8, 256, 256, 0.5, 0.5, 4.0)
canvas = prepare_reframe_canvas(frames, placement)
outpaint, source = build_reframe_masks(placement, feather=32)
self.assertEqual(tuple(canvas.shape), (2, 256, 256, 3))
self.assertTrue(torch.allclose(canvas, torch.ones_like(canvas)))
self.assertTrue(torch.allclose(source, torch.ones_like(source)))
self.assertTrue(torch.allclose(outpaint, torch.zeros_like(outpaint)))
def test_target_fit_returns_exact_dimensions(self):
images = torch.rand((2, 96, 160, 3), dtype=torch.float32)
fitted, = LTXReframeTargetFit().fit(images, target_width=128, target_height=128)
self.assertEqual(tuple(fitted.shape), (2, 128, 128, 3))
self.assertGreaterEqual(fitted.min().item(), 0.0)
self.assertLessEqual(fitted.max().item(), 1.0)
def test_rectilinear_correction_is_temporally_deterministic(self):
horizontal = torch.linspace(0.0, 1.0, 65).view(1, 1, 65, 1)
frame = horizontal.expand(1, 33, 65, 3)
images = frame.expand(2, -1, -1, -1).clone()
corrected, correction_mask = rectilinear_correct_images(
images,
correction_strength=0.1,
center_x=0.5,
center_y=0.5,
protect_source=0.0,
protection_feather=0,
interpolation="bilinear",
chunk_size=1,
)
self.assertEqual(tuple(corrected.shape), tuple(images.shape))
self.assertEqual(tuple(correction_mask.shape), (1, 33, 65))
self.assertTrue(torch.allclose(corrected[0], corrected[1]))
self.assertGreater(corrected[0, 16, 0, 0].item(), images[0, 16, 0, 0].item())
self.assertLess(corrected[0, 16, -1, 0].item(), images[0, 16, -1, 0].item())
self.assertAlmostEqual(
corrected[0, 16, 32, 0].item(),
images[0, 16, 32, 0].item(),
places=5,
)
def test_rectilinear_source_mask_can_fully_protect_original_pixels(self):
images = torch.rand((2, 24, 40, 3), dtype=torch.float32)
source_mask = torch.ones((1, 24, 40), dtype=torch.float32)
corrected, correction_mask = rectilinear_correct_images(
images,
correction_strength=0.15,
center_x=0.5,
center_y=0.5,
protect_source=1.0,
protection_feather=0,
interpolation="bicubic",
chunk_size=2,
source_mask=source_mask,
)
self.assertTrue(torch.allclose(corrected, images))
self.assertEqual(torch.count_nonzero(correction_mask).item(), 0)
def test_rectilinear_node_bypass_is_pixel_exact(self):
images = torch.rand((3, 16, 20, 3), dtype=torch.float32)
bypassed, correction_mask = LTXReframeRectilinearCorrection().correct(
images,
enabled=False,
correction_strength=0.05,
center_x=0.5,
center_y=0.5,
protect_source=1.0,
protection_feather=96,
interpolation="bicubic",
chunk_size=8,
)
self.assertIs(bypassed, images)
self.assertEqual(tuple(correction_mask.shape), (1, 16, 20))
self.assertEqual(torch.count_nonzero(correction_mask).item(), 0)
if __name__ == "__main__":
unittest.main()
+89
View File
@@ -0,0 +1,89 @@
import assert from "node:assert/strict";
let registeredExtension = null;
globalThis.window = {
comfyAPI: {
app: {
app: {
registerExtension(extension) {
registeredExtension = extension;
},
},
},
},
};
await import(`../web/js/model_loader.js?test=${Date.now()}`);
assert.ok(registeredExtension, "the model-loader frontend extension should register");
class UnrelatedNode {}
await registeredExtension.beforeRegisterNodeDef(UnrelatedNode, { name: "OtherNode" });
assert.equal(
UnrelatedNode.prototype.onNodeCreated,
undefined,
"unrelated node types should not be patched",
);
let createdCalls = 0;
let configuredCalls = 0;
class FakeLoaderNode {}
FakeLoaderNode.prototype.onNodeCreated = function () {
createdCalls += 1;
};
FakeLoaderNode.prototype.onConfigure = function () {
configuredCalls += 1;
};
await registeredExtension.beforeRegisterNodeDef(FakeLoaderNode, {
name: "DiffusionGemmaModelLoader",
});
const hiddenWidgetNames = new Set([
"backend",
"dtype",
"quantization",
"local_files_only",
"unload_policy",
"max_memory_gb",
]);
const node = new FakeLoaderNode();
node.widgets = [
{ name: "model_path", value: "models/diffusiongemma" },
{ name: "temperature", value: "invalid" },
...[...hiddenWidgetNames].map((name) => ({ name, value: "advanced" })),
];
node.size = [520, 640];
node.computeSize = () => [360, 240];
node.setSize = (size) => {
node.size = [...size];
};
node.onNodeCreated();
assert.equal(createdCalls, 1, "the original creation hook should still run");
assert.equal(
node.widgets.find((entry) => entry.name === "temperature").value,
0.45,
"invalid saved temperatures should migrate to the stable default",
);
assert.deepEqual(node.size, [520, 240], "hiding widgets should preserve the user's width");
for (const entry of node.widgets) {
if (hiddenWidgetNames.has(entry.name)) {
assert.equal(entry.hidden, true, `${entry.name} should be hidden`);
assert.equal(entry.__dgHidden, true, `${entry.name} should only be hidden once`);
assert.deepEqual(entry.computeSize(), [0, -4]);
} else {
assert.notEqual(entry.hidden, true, `${entry.name} should remain visible`);
}
}
node.widgets.find((entry) => entry.name === "temperature").value = 0.7;
node.size = [610, 700];
node.computeSize = () => [380, 260];
node.onConfigure({});
assert.equal(configuredCalls, 1, "the original configuration hook should still run");
assert.equal(node.widgets.find((entry) => entry.name === "temperature").value, 0.7);
assert.deepEqual(node.size, [610, 260], "configuration should keep the saved width");
console.log("DiffusionGemma model-loader UI checks passed");
+38
View File
@@ -0,0 +1,38 @@
from __future__ import annotations
from pathlib import Path
import tomllib
import unittest
ROOT = Path(__file__).resolve().parents[1]
class PackageMetadataTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.metadata = tomllib.loads(
(ROOT / "pyproject.toml").read_text(encoding="utf-8")
)
def test_registry_identity_and_release_version_are_current(self) -> None:
self.assertEqual(self.metadata["project"]["version"], "0.2.0")
self.assertEqual(self.metadata["tool"]["comfy"]["PublisherId"], "exportanything")
def test_registry_compatibility_starts_at_the_tested_baseline(self) -> None:
self.assertEqual(
self.metadata["tool"]["comfy"]["requires-comfyui"],
">=0.30.1",
)
def test_registry_archive_excludes_ci_and_large_demo_media(self) -> None:
ignored = {
line.strip()
for line in (ROOT / ".comfyignore").read_text(encoding="utf-8").splitlines()
if line.strip() and not line.lstrip().startswith("#")
}
self.assertTrue({".github/", "tests/", "*.mp4", "*.gif"} <= ignored)
if __name__ == "__main__":
unittest.main()
+437
View File
@@ -0,0 +1,437 @@
from __future__ import annotations
import importlib.util
import json
import sys
import unittest
import uuid
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
COMFY_ROOT = ROOT.parents[1]
SCHEMA = ROOT / "schemas" / "splatstage_show_blueprint.schema.json"
DIRECTOR_NODE_IDS = {
"DiffusionGemmaModelLoader",
"DiffusionGemmaContextHub",
"DiffusionGemmaH3ReferenceContext",
"DiffusionGemmaTargetProfile",
"DiffusionGemmaCoTGenerator",
"DiffusionGemmaJSONSplitter",
"DiffusionGemmaGenerationGate",
}
EXPERIMENTAL_NODE_IDS = {
"DiffusionGemmaSplatStagePlanner",
"DiffusionGemmaSplatStageEfficientPlanner",
}
def load_nodes_module():
if str(COMFY_ROOT) not in sys.path:
sys.path.insert(0, str(COMFY_ROOT))
package_name = f"diffusiongemma_splatstage_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 SplatStagePlannerMusicTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.nodes = load_nodes_module()
cls.schema = json.loads(SCHEMA.read_text(encoding="utf-8"))
def test_strict_schema_is_bundled_with_the_package(self) -> None:
self.assertEqual(self.nodes._splatstage_blueprint_schema_path(), SCHEMA)
def test_prompt_requests_a_full_song_before_the_video_excerpt(self) -> None:
prompt = self.nodes._splatstage_blueprint_prompt(
"make a pop-music video",
42,
self.schema,
duration_seconds=20.0,
aspect_ratio="16:9",
)
self.assertIn("complete 90-second source song", prompt)
self.assertIn("final video uses a 20-second excerpt", prompt)
self.assertIn("two separately generated performer clips", prompt)
self.assertIn("one performer object, one background object, and one FX object", prompt)
self.assertIn("never arrays or clip-specific lists", prompt)
self.assertIn("16 to 36 non-empty lines", prompt)
self.assertIn("SAM3 object-category bundle", prompt)
self.assertIn("wide-brimmed mariachi hat", prompt)
self.assertIn("electric guitar", prompt)
self.assertIn("positive_prompt must use affirmative visual language only", prompt)
self.assertIn('"schema":"splatstage.show_blueprint"', prompt)
self.assertIn('"version":1', prompt)
self.assertIn(
'Never put the schema document identifier "splatstage.show_blueprint@1"',
prompt,
)
def test_planner_contract_supports_ten_or_twenty_seconds(self) -> None:
spec = self.nodes.DiffusionGemmaSplatStagePlanner.INPUT_TYPES()["required"][
"duration_seconds"
][1]
self.assertEqual(spec["min"], 10.0)
self.assertEqual(spec["max"], 20.0)
self.assertEqual(spec["default"], 20.0)
def test_full_song_lyric_budget_accepts_48_lines_but_not_49(self) -> None:
def blueprint(line_count: int) -> dict:
return {
"schema": "splatstage.show_blueprint",
"version": 1,
"project_title": "Song",
"creative_summary": "A compact performance video.",
"music": {
"ace_tags": "bright pop",
"vocal_mode": "vocal_hook",
"lyrics": "\n".join(
f"Original lyric line {index}" for index in range(line_count)
),
"bpm": 120,
"key": "C major",
"time_signature": "4",
"language": "en",
},
"performer": {
"label": "Singer",
"positive_prompt": "One singer performing",
"negative_prompt": "crowd",
"segmentation_prompt": "the singer",
},
"background": {
"positive_prompt": "Abstract lights",
"negative_prompt": "people",
},
"fx": {
"positive_prompt": "Prism sparks",
"negative_prompt": "people, text",
"blend_mode": "screen",
"key_mode": "luminance",
"opacity": 0.5,
},
"editing": {
"background_density": "musical",
"fx_density": "musical",
"music_reactivity": 0.5,
},
"finish": {"style_preset": "broadcast_clean"},
}
self.assertEqual(
self.nodes._splatstage_blueprint_errors(
blueprint(48), self.schema
),
[],
)
self.assertTrue(
any(
"at most 48" in error
for error in self.nodes._splatstage_blueprint_errors(
blueprint(49), self.schema
)
)
)
def test_segmentation_prompt_uses_separate_sam3_object_categories(self) -> None:
valid = {
"schema": "splatstage.show_blueprint",
"version": 1,
"project_title": "Mariachi",
"creative_summary": "A cyberpunk mariachi performance.",
"music": {
"ace_tags": "cyberpunk mariachi",
"vocal_mode": "instrumental",
"lyrics": "[Instrumental]",
"bpm": 128,
"key": "G minor",
"time_signature": "4",
"language": "en",
},
"performer": {
"label": "Lead mariachi",
"positive_prompt": "One cyberpunk mariachi playing a glowing guitar",
"negative_prompt": "crowd",
"segmentation_prompt": (
"cyberpunk mariachi musician, black charro suit, "
"wide-brimmed mariachi hat, glowing electric guitar"
),
},
"background": {
"positive_prompt": "Martian landscape",
"negative_prompt": "people",
},
"fx": {
"positive_prompt": "isolated sparks",
"negative_prompt": "people, text",
"blend_mode": "screen",
"key_mode": "luminance",
"opacity": 0.5,
},
"editing": {
"background_density": "musical",
"fx_density": "musical",
"music_reactivity": 0.5,
},
"finish": {"style_preset": "broadcast_clean"},
}
self.assertEqual(
self.nodes._splatstage_blueprint_errors(valid, self.schema),
[],
)
leaked = json.loads(json.dumps(valid))
leaked["fx"]["positive_prompt"] = "Neon sparks, no words, text-free overlay"
polarity_errors = self.nodes._splatstage_blueprint_errors(leaked, self.schema)
self.assertTrue(any("affirmative visual language" in error for error in polarity_errors))
invalid = json.loads(json.dumps(valid))
invalid["performer"]["segmentation_prompt"] = (
"the musician in the charro suit and guitar"
)
self.assertTrue(
any(
"separate the performer" in error
for error in self.nodes._splatstage_blueprint_errors(
invalid, self.schema
)
)
)
def test_two_option_lane_lists_are_seed_addressed_to_one_coherent_contract(self) -> None:
candidate = {
"performer": [{"label": "firefighter"}, {"label": "country singer"}],
"background": [{"name": "fire station"}, {"name": "burning skyline"}],
"fx": [{"name": "embers"}, {"name": "smoke"}],
}
normalized, report = self.nodes._normalize_splatstage_lane_cardinality(
candidate,
selection_index=5,
)
self.assertEqual(normalized["performer"]["label"], "country singer")
self.assertEqual(normalized["background"]["name"], "burning skyline")
self.assertEqual(normalized["fx"]["name"], "smoke")
self.assertEqual(report["performer"]["selected_index"], 1)
self.assertIsInstance(candidate["performer"], list)
def test_malformed_or_oversized_lane_lists_remain_invalid(self) -> None:
candidate = {
"performer": [{"label": "a"}, {"label": "b"}, {"label": "c"}],
"background": [],
"fx": ["not an object"],
}
normalized, report = self.nodes._normalize_splatstage_lane_cardinality(
candidate,
selection_index=0,
)
self.assertEqual(normalized, candidate)
self.assertEqual(report, {})
def test_schema_document_id_alias_is_canonicalized_only_for_matching_version(self) -> None:
candidate = {
"schema": "splatstage.show_blueprint@1",
"version": 1,
"project_title": "Alias candidate",
}
normalized, report = self.nodes._normalize_splatstage_blueprint_identity(
candidate,
self.schema,
)
self.assertEqual(normalized["schema"], "splatstage.show_blueprint")
self.assertEqual(report["input_schema"], "splatstage.show_blueprint@1")
self.assertEqual(candidate["schema"], "splatstage.show_blueprint@1")
wrong_version = dict(candidate, version=2)
unchanged, report = self.nodes._normalize_splatstage_blueprint_identity(
wrong_version,
self.schema,
)
self.assertEqual(unchanged, wrong_version)
self.assertEqual(report, {})
unknown_alias = dict(candidate, schema="splatstage.show_blueprint@999")
unchanged, report = self.nodes._normalize_splatstage_blueprint_identity(
unknown_alias,
self.schema,
)
self.assertEqual(unchanged, unknown_alias)
self.assertEqual(report, {})
class EfficientSplatStagePlannerTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.nodes = load_nodes_module()
cls.schema = json.loads(
(ROOT / "schemas" / "splatstage_efficient_show_blueprint.schema.json").read_text(
encoding="utf-8"
)
)
@staticmethod
def valid_blueprint() -> dict:
return {
"schema": "splatstage.efficient_show_blueprint",
"version": 1,
"project_title": "Cyber Mariachi",
"creative_summary": "A cyberpunk mariachi performance on Mars.",
"music": {
"ace_tags": "cyber mariachi pop, vocal hook",
"vocal_mode": "vocal_hook",
"lyrics": "[Verse]\nRed dust in the light\n[Chorus]\nPlay it to the stars",
"bpm": 124,
"key": "C major",
"time_signature": "4",
"language": "en",
},
"performer": {
"identity_description": "One cyberpunk mariachi guitarist in a black charro suit",
"positive_prompt": "Full-body performance beneath magenta concert light",
"negative_prompt": "crowd, duplicate person, text, logos",
"segmentation_prompt": "mariachi guitarist, mariachi hat, charro suit, electric guitar",
"variations": [
"Low-angle strumming with a slow dolly",
"Energetic side view with rhythmic footwork",
"Centered hero framing with sweeping arm motion",
],
},
"background": {
"positive_prompt": "Martian neon concert architecture and red dust",
"negative_prompt": "people, performers, crowd, text, logos",
"variations": [
"Wide canyon stage with drifting haze",
"Graphic orbital city vista with slow parallax",
],
},
"fx": {
"positive_prompt": "Isolated luminous prismatic guitar-wave geometry on a pure black background",
"negative_prompt": "people, faces, text, words, letters, logos, symbols",
"blend_mode": "screen",
"key_mode": "luminance",
"opacity": 0.55,
},
"editing": {"density": "musical", "music_reactivity": "medium"},
"finish": {"style_preset": "broadcast_clean"},
}
def test_schema_requires_exact_candidate_counts_and_affirmative_prompts(self) -> None:
value = self.valid_blueprint()
self.assertEqual(
self.nodes._splatstage_efficient_blueprint_errors(value, self.schema), []
)
value["performer"]["variations"] = value["performer"]["variations"][:2]
value["fx"]["positive_prompt"] = "Glowing particles, no words, text-free"
errors = self.nodes._splatstage_efficient_blueprint_errors(value, self.schema)
self.assertTrue(any("variations" in error for error in errors))
self.assertTrue(any("affirmative" in error for error in errors))
def test_prompt_locks_full_song_short_assets_and_semantic_union(self) -> None:
prompt = self.nodes._splatstage_efficient_blueprint_prompt(
"make a pop-music video", 42, self.schema
)
self.assertIn("complete 90-second song", prompt)
self.assertIn("three separate five-second performer videos", prompt)
self.assertIn("canonical performer identity first", prompt)
self.assertIn("only concrete silhouette-extending items actually present", prompt)
self.assertIn("richly art-directed luminous motif", prompt)
self.assertIn("limited color palette", prompt)
self.assertIn("rhythmic energy or", prompt)
self.assertIn("clear spatial composition", prompt)
self.assertIn("Do not name TouchDesigner", prompt)
self.assertIn("locked TouchDesigner-inspired", prompt)
self.assertIn("affirmative visual language", prompt)
def test_fx_creative_slot_rejects_software_or_interface_language(self) -> None:
value = self.valid_blueprint()
value["fx"]["positive_prompt"] = (
"TouchDesigner application screenshot with a cyan particle vortex"
)
errors = self.nodes._splatstage_efficient_blueprint_errors(value, self.schema)
self.assertTrue(
any("locked TouchDesigner-inspired" in error for error in errors)
)
def test_efficient_segmentation_rejects_free_standing_generic_categories(self) -> None:
value = self.valid_blueprint()
value["performer"]["segmentation_prompt"] = (
"mariachi guitarist, face and head, hair and worn headwear, "
"costume and worn accessories, held instrument and attached props"
)
errors = self.nodes._splatstage_efficient_blueprint_errors(value, self.schema)
self.assertTrue(any("identity-specific items" in error for error in errors))
def test_efficient_segmentation_uses_identity_and_specific_items_only(self) -> None:
value = self.valid_blueprint()
value["performer"]["segmentation_prompt"] = (
"cyberpunk mariachi guitarist, face and head, hair and worn headwear, "
"black charro suit, electric guitar"
)
normalized = self.nodes._normalize_splatstage_efficient_segmentation(value)
segmentation = normalized["performer"]["segmentation_prompt"]
self.assertTrue(
segmentation.startswith("One cyberpunk mariachi guitarist in a black charro suit")
)
self.assertNotIn("face and head", segmentation)
self.assertNotIn("hair and worn headwear", segmentation)
self.assertIn("black charro suit", segmentation)
self.assertIn("electric guitar", segmentation)
self.assertLessEqual(len(segmentation.split(",")), 6)
self.assertEqual(
self.nodes._splatstage_efficient_blueprint_errors(normalized, self.schema),
[],
)
self.assertEqual(
normalized["performer"]["positive_prompt"],
value["performer"]["positive_prompt"],
)
def test_efficient_planner_is_registered_with_safe_widget_contract(self) -> None:
self.assertIs(
self.nodes.NODE_CLASS_MAPPINGS["DiffusionGemmaSplatStageEfficientPlanner"],
self.nodes.DiffusionGemmaSplatStageEfficientPlanner,
)
required = self.nodes.DiffusionGemmaSplatStageEfficientPlanner.INPUT_TYPES()[
"required"
]
self.assertEqual(required["max_new_tokens"][1]["default"], 3072)
self.assertGreaterEqual(required["max_new_tokens"][1]["min"], 1024)
self.assertNotIn("duration_seconds", required)
class ExportedNodeSurfaceTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.nodes = load_nodes_module()
def test_export_map_has_seven_director_and_two_experimental_nodes(self) -> None:
expected = DIRECTOR_NODE_IDS | EXPERIMENTAL_NODE_IDS
self.assertEqual(set(self.nodes.NODE_CLASS_MAPPINGS), expected)
self.assertEqual(set(self.nodes.NODE_DISPLAY_NAME_MAPPINGS), expected)
def test_experimental_planners_are_visibly_separate(self) -> None:
for node_id in DIRECTOR_NODE_IDS:
self.assertEqual(
self.nodes.NODE_CLASS_MAPPINGS[node_id].CATEGORY,
self.nodes.CATEGORY,
)
for node_id in EXPERIMENTAL_NODE_IDS:
self.assertEqual(
self.nodes.NODE_CLASS_MAPPINGS[node_id].CATEGORY,
self.nodes.EXPERIMENTAL_MOTION_CATEGORY,
)
self.assertTrue(
self.nodes.NODE_DISPLAY_NAME_MAPPINGS[node_id].endswith("(Experimental)")
)
if __name__ == "__main__":
unittest.main()