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

1504 lines
62 KiB
Python

from __future__ import annotations
import importlib.util
import json
import re
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_ltx25_contracts_{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 Ltx25ContractTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.nodes = load_nodes_module()
cls.contract = sys.modules[f"{cls.nodes.__package__}.ltx25_contract"]
@staticmethod
def frame_ledger(*categories_and_claims: tuple[list[str], str]) -> dict:
facts = []
for index, (categories, claim) in enumerate(categories_and_claims, start=1):
facts.append(
{
"fact_id": f"fact-{index}",
"claim": claim,
"confidence": "high",
"categories": categories,
"evidence": [{"asset_id": "image:1", "sample_ordinal": 1}],
}
)
return {
"schema": "dg-grounding-ledger/1",
"analysis_status": "grounded",
"observed_facts": facts,
"inferred_facts": [],
"creative_additions": [],
"uncertainties": [],
"grounding_failure_reasons": [],
}
@classmethod
def conditioned_metadata(cls, *, first: bool = True, last: bool = False, ledger=None) -> dict:
metadata = {
"source": "image" if first or last else "none",
"ltx25_context_schema": cls.nodes.LTX25_CONTRACT_SCHEMA,
"ltx_first_frame_attached": first,
"ltx_last_frame_attached": last,
"ltx_frame_pair_attached": first and last,
}
if ledger is not None:
metadata["verified_grounding_ledger"] = ledger
return metadata
@staticmethod
def prompt_packet(*, ltx_prompt: str = "", ideogram_prompt: str = "") -> str:
return json.dumps(
{
"ltx_prompt": ltx_prompt,
"ideogram_prompt": ideogram_prompt,
"minimax_h3_prompt": "",
"negative_prompt": "",
"scene_segments": [],
"metadata": {"json_parse_valid": True},
}
)
def test_mode_normalization_accepts_ui_labels_aliases_and_legacy_video(self) -> None:
cases = {
None: "auto",
"Auto (recommended)": "auto",
"T2V": "text_to_video",
"Text to video": "text_to_video",
"I2V": "image_to_video",
"Image-to-video": "image_to_video",
"FLF": "first_last_frame",
"First + last frame": "first_last_frame",
"legacy video": "legacy_video",
}
for value, expected in cases.items():
with self.subTest(value=value):
self.assertEqual(self.nodes.normalize_ltx25_generation_mode(value), expected)
def test_auto_mode_resolution_distinguishes_t2v_i2v_flf_and_legacy_video(self) -> None:
cases = (
({"source": "none"}, 0, "text_to_video", "no_conditioning_frames"),
(
self.conditioned_metadata(first=True),
1,
"image_to_video",
"attached_frames",
),
(
self.conditioned_metadata(first=True, last=True),
2,
"first_last_frame",
"attached_frames",
),
({"source": "video"}, 8, "legacy_video", "legacy_video_context"),
)
for metadata, image_count, expected_mode, expected_source in cases:
with self.subTest(expected_mode=expected_mode):
report = self.nodes.resolve_ltx25_generation_mode("auto", metadata, image_count)
self.assertEqual(report["resolved_mode"], expected_mode)
self.assertEqual(report["resolution_source"], expected_source)
generic_image = self.nodes.resolve_ltx25_generation_mode(
"auto", {"source": "image"}, image_count=1
)
self.assertEqual(generic_image["resolved_mode"], "image_to_video")
self.assertIn("ltx_mode_inferred_from_generic_image_context", generic_image["warnings"])
def test_explicit_mode_overrides_context_hint_but_reports_the_mismatch(self) -> None:
metadata = self.conditioned_metadata(first=True)
metadata["ltx_generation_mode_hint"] = "image_to_video"
report = self.nodes.resolve_ltx25_generation_mode("Text to video", metadata, 1)
self.assertEqual(report["resolved_mode"], "text_to_video")
self.assertEqual(report["resolution_source"], "target_profile")
self.assertIn("ltx_explicit_mode_overrides_context_hint", report["warnings"])
def test_context_hub_appends_last_frame_socket_without_reordering_legacy_widgets(self) -> None:
inputs = self.nodes.DiffusionGemmaContextHub.INPUT_TYPES()
self.assertEqual(
list(inputs["required"]),
[
"user_prompt",
"sample_fps",
"max_duration_seconds",
"max_frames",
"overlong_policy",
"include_image_with_video",
],
)
self.assertEqual(
list(inputs["optional"]),
["image", "video", "visual_description", "media_synthesis_mode", "last_frame_image"],
)
# This is the complete positional signature saved before last_frame_image
# was appended. It must continue to bind media_synthesis_mode correctly.
context, context_json, _preview = self.nodes.DiffusionGemmaContextHub().build(
"A person waits beside a window.",
1.0,
60.0,
60,
"trim",
True,
None,
None,
"",
"video_recreation",
)
payload = json.loads(context_json)
self.assertEqual(context.user_prompt, "A person waits beside a window.")
self.assertEqual(context.media_metadata["ltx_generation_mode_hint"], "text_to_video")
self.assertFalse(context.media_metadata["ltx_first_frame_attached"])
self.assertFalse(context.media_metadata["ltx_last_frame_attached"])
self.assertEqual(payload["media"]["ltx_generation_mode_hint"], "text_to_video")
def test_context_hub_emits_first_and_last_frame_roles_and_geometry(self) -> None:
first = torch.zeros((1, 12, 20, 3), dtype=torch.float32)
last = torch.ones((1, 8, 14, 3), dtype=torch.float32)
i2v_context, _json, _preview = self.nodes.DiffusionGemmaContextHub().build(
"She turns toward the doorway.", image=first
)
self.assertEqual(i2v_context.media_metadata["ltx_generation_mode_hint"], "image_to_video")
self.assertEqual(i2v_context.media_metadata["ltx_frame_roles"], ["first_frame"])
self.assertEqual(i2v_context.media_metadata["ltx_first_frame_width"], 20)
self.assertEqual(i2v_context.media_metadata["ltx_first_frame_height"], 12)
self.assertEqual(tuple(i2v_context.images.shape), (1, 12, 20, 3))
flf_context, _json, _preview = self.nodes.DiffusionGemmaContextHub().build(
"She crosses the room and reaches the doorway.",
image=first,
last_frame_image=last,
)
metadata = flf_context.media_metadata
self.assertEqual(metadata["ltx_generation_mode_hint"], "first_last_frame")
self.assertTrue(metadata["ltx_frame_pair_attached"])
self.assertEqual(metadata["ltx_frame_roles"], ["first_frame", "last_frame"])
self.assertEqual(metadata["reference_image_count"], 2)
self.assertTrue(metadata["reference_image_backend_attached"])
self.assertEqual(metadata["ltx_first_frame_width"], 20)
self.assertEqual(metadata["ltx_first_frame_height"], 12)
self.assertEqual(metadata["ltx_last_frame_width"], 14)
self.assertEqual(metadata["ltx_last_frame_height"], 8)
self.assertEqual(tuple(flf_context.images.shape), (2, 12, 20, 3))
def test_each_mode_has_a_unique_compiler_contract_in_the_model_prompt(self) -> None:
modes_and_markers = {
"text_to_video": "This is text-to-video with no conditioning frame.",
"image_to_video": "This is image-to-video.",
"first_last_frame": "This is first-and-last-frame video.",
"legacy_video": "This is a legacy source-video recreation",
}
contracts = {}
for mode, marker in modes_and_markers.items():
with self.subTest(mode=mode):
contract = self.nodes.ltx25_compiler_contract(mode, 8.0)
contracts[mode] = contract
self.assertIn(marker, contract)
self.assertIn("The clip lasts 8.000 seconds.", contract)
self.assertRegex(
contract,
r"\bphysically continuous(?: subject-relative)? path\b",
)
self.assertIn("Compatible simultaneous or sequential motion phases", contract)
self.assertNotIn("camera move(s)", contract)
metadata = {"source": "none"}
if mode == "image_to_video":
metadata = self.conditioned_metadata(first=True)
elif mode == "first_last_frame":
metadata = self.conditioned_metadata(first=True, last=True)
elif mode == "legacy_video":
metadata = {"source": "video", "sampled_frame_count": 4}
model_prompt = self.nodes._build_model_prompt(
"A concise test scene.",
"Return only the requested JSON.",
"ltx",
metadata,
target_duration_seconds=8.0,
ltx_generation_mode=mode,
)
self.assertIn(marker, model_prompt)
self.assertNotIn("max_camera_moves", model_prompt)
self.assertEqual(len(set(contracts.values())), len(contracts))
def test_ltx_model_output_shape_keeps_host_owned_fields_compact(self) -> None:
model_prompt = self.nodes._build_model_prompt(
"A concise test scene.",
"Return only the requested JSON.",
"ltx",
self.conditioned_metadata(first=True),
target_duration_seconds=5.0,
ltx_generation_mode="image_to_video",
)
shape_text = model_prompt.split(
"Return the final answer as JSON matching this shape:\n", 1
)[1].split("\n\nCritical output rules:", 1)[0]
output_shape = json.loads(shape_text)
self.assertIsInstance(output_shape["ltx_prompt"], str)
self.assertTrue(output_shape["ltx_prompt"])
self.assertEqual(output_shape["ideogram_prompt"], "")
self.assertEqual(output_shape["minimax_h3_prompt"], "")
self.assertEqual(output_shape["scene_segments"], [])
self.assertEqual(output_shape["metadata"], {})
self.assertNotIn('"controls"', shape_text)
verified_metadata = self.conditioned_metadata(
first=True,
ledger=self.frame_ledger(
(["identity", "appearance"], "An officer in a dark uniform is visible."),
),
)
verified_metadata.update(
{
"grounding_evidence_report_id": "dggr-test",
"grounding_required_asset_ids": ["image:1"],
}
)
verified_prompt = self.nodes._build_model_prompt(
"The officer takes one measured step.",
"Return only the requested JSON.",
"ltx",
verified_metadata,
target_duration_seconds=5.0,
ltx_generation_mode="image_to_video",
)
verified_shape = json.loads(
verified_prompt.split(
"Return the final answer as JSON matching this shape:\n", 1
)[1].split("\n\nCritical output rules:", 1)[0]
)
self.assertEqual(
set(verified_shape["metadata"]),
{"grounding_evidence_report_id", "used_grounding_fact_ids"},
)
def test_audit_contract_scales_fact_limit_to_attached_assets(self) -> None:
one_image = {
"assets": [
{
"asset_id": "image:1",
"kind": "image",
"samples": [{"sample_ordinal": 1}],
}
]
}
prompt = self.nodes._audit_grounding_master_prompt(one_image)
self.assertIn("Limit observed facts to the 4 most useful facts", prompt)
self.assertNotIn("24 most useful facts", prompt)
four_images = {
"assets": [
{
"asset_id": f"image:{index}",
"kind": "image",
"samples": [{"sample_ordinal": index}],
}
for index in range(1, 5)
]
}
self.assertIn(
"Limit observed facts to the 8 most useful facts",
self.nodes._audit_grounding_master_prompt(four_images),
)
def test_i2v_context_image_roles_are_sanitized_for_the_model_prompt(self) -> None:
first = torch.zeros((1, 12, 20, 3), dtype=torch.float32)
context, _context_json, _preview = self.nodes.DiffusionGemmaContextHub().build(
"She slowly turns toward the doorway.",
image=first,
)
self.assertIn("[dg:", context.media_metadata["image_roles"][0])
model_prompt = self.nodes._build_model_prompt(
context.user_prompt,
"Return only the requested JSON.",
"ltx",
context.media_metadata,
target_duration_seconds=5.0,
ltx_generation_mode="auto",
)
self.assertIn("LTX first-frame anchor", model_prompt)
self.assertNotIn("[dg:", model_prompt)
self.assertIn("[dg:", context.media_metadata["image_roles"][0])
def test_conditioned_modes_report_missing_anchors(self) -> None:
i2v_report = self.nodes.resolve_ltx25_generation_mode(
"image_to_video", self.conditioned_metadata(first=False), 0
)
i2v = self.nodes.validate_ltx25_prompt(
"A static medium shot holds on the empty room.", i2v_report, {}, 5.0
)
self.assertIn("ltx_i2v_first_frame_missing", i2v["reasons"])
self.assertFalse(i2v["ready"])
flf_report = self.nodes.resolve_ltx25_generation_mode(
"first_last_frame", self.conditioned_metadata(first=False, last=False), 0
)
flf = self.nodes.validate_ltx25_prompt(
"A static medium shot holds on the empty room.", flf_report, {}, 5.0
)
self.assertIn("ltx_flf_first_frame_missing", flf["reasons"])
self.assertIn("ltx_flf_last_frame_missing", flf["reasons"])
first_only_metadata = self.conditioned_metadata(first=True, last=False)
first_only_report = self.nodes.resolve_ltx25_generation_mode(
"first_last_frame", first_only_metadata, 1
)
first_only = self.nodes.validate_ltx25_prompt(
"A static close-up holds on the subject.", first_only_report, first_only_metadata, 5.0
)
self.assertNotIn("ltx_flf_first_frame_missing", first_only["reasons"])
self.assertIn("ltx_flf_last_frame_missing", first_only["reasons"])
def test_i2v_rejects_cuts_and_multiple_shots(self) -> None:
metadata = self.conditioned_metadata(first=True)
mode_report = self.nodes.resolve_ltx25_generation_mode("image_to_video", metadata, 1)
validation = self.nodes.validate_ltx25_prompt(
"A static close-up holds on the woman. A hard cut to a wide shot reveals the street.",
mode_report,
metadata,
8.0,
)
self.assertIn("ltx_conditioned_mode_requires_single_continuous_take", validation["reasons"])
self.assertIn("ltx_shot_count_exceeds_duration_budget", validation["reasons"])
self.assertGreater(validation["complexity"]["observed"]["cut_count"], 0)
def test_duration_budgets_scale_t2v_but_keep_conditioned_modes_to_one_take(self) -> None:
expected_t2v_shots = {4.0: 1, 8.0: 2, 12.0: 3, 15.0: 4}
for duration, expected_shots in expected_t2v_shots.items():
with self.subTest(duration=duration):
budget = self.nodes.ltx25_complexity_budget("text_to_video", duration)
self.assertEqual(budget["max_shots"], expected_shots)
self.assertEqual(budget["max_cuts"], expected_shots - 1)
for mode in ("image_to_video", "first_last_frame"):
budget = self.nodes.ltx25_complexity_budget(mode, 20.0)
self.assertEqual(budget["max_shots"], 1)
self.assertEqual(budget["max_cuts"], 0)
self.assertNotIn("max_camera_moves", budget)
self.assertEqual(budget["recommended_camera_motion_phases"], 4)
terminal_black = self.contract.analyze_ltx25_prompt_complexity(
"A static medium shot at eye level holds on the woman, then the image cuts to black.",
"text_to_video",
4.0,
)
self.assertEqual(terminal_black["observed"]["cut_count"], 0)
self.assertEqual(terminal_black["observed"]["estimated_shots"], 1)
self.assertNotIn("ltx_shot_scale_missing", terminal_black["reasons"])
self.assertNotIn("ltx_camera_state_missing", terminal_black["reasons"])
self.assertNotIn("ltx_viewpoint_missing", terminal_black["reasons"])
overloaded = self.contract.analyze_ltx25_prompt_complexity(
(
"A wide shot shows a runner. A hard cut to a medium shot follows as the camera pans, "
"then dollies, then tilts. She turns, walks, runs, jumps, falls, opens a gate, enters, "
"exits, and disappears. Music, footsteps, heartbeat, wind, rain, and a siren overlap. "
'A narrator says, "There is far too much dialogue here to fit clearly in four seconds."'
),
"text_to_video",
4.0,
)
self.assertTrue(
{
"ltx_shot_count_exceeds_duration_budget",
"ltx_action_density_exceeds_duration_budget",
"ltx_audio_density_exceeds_duration_budget",
"ltx_speech_exceeds_duration_budget",
}.issubset(set(overloaded["reasons"]))
)
self.assertNotIn("ltx_camera_complexity_exceeds_duration_budget", overloaded["reasons"])
self.assertIn("ltx_camera_complexity_at_duration_limit", overloaded["warnings"])
def test_conditioned_viewpoint_continuity_is_not_mistaken_for_missing_angle(self) -> None:
live_caption = (
"In a medium close-up, an officer stands by a roadside during daylight. "
"The first frame shows him centered beneath soft overcast light. "
"The camera starts with this static framing and begins a subtle stabilized push. "
"He shifts his weight and takes one measured step while roadside ambience continues."
)
i2v = self.contract.analyze_ltx25_prompt_complexity(
live_caption, "image_to_video", 5.0
)
self.assertEqual(i2v["observed"]["conditioned_viewpoint_anchor_count"], 1)
self.assertNotIn("ltx_viewpoint_missing", i2v["reasons"])
t2v = self.contract.analyze_ltx25_prompt_complexity(
live_caption, "text_to_video", 5.0
)
self.assertEqual(t2v["observed"]["conditioned_viewpoint_anchor_count"], 0)
self.assertIn("ltx_viewpoint_missing", t2v["reasons"])
unanchored_i2v = self.contract.analyze_ltx25_prompt_complexity(
"A static medium close-up centers an officer beside a road.",
"image_to_video",
5.0,
)
self.assertNotIn("ltx_viewpoint_missing", unanchored_i2v["reasons"])
def test_conditioned_viewpoint_is_inherited_but_t2v_stays_strict(self) -> None:
caption = (
"A medium shot frames an astronaut holding a lollipop while the camera remains static. "
"He raises the candy as nearby creatures sway gently and soft music continues."
)
i2v_report = self.contract.resolve_ltx25_generation_mode(
"image_to_video",
self.conditioned_metadata(first=True),
1,
)
i2v = self.contract.validate_ltx25_prompt(
caption,
i2v_report,
self.conditioned_metadata(first=True),
5.0,
)
self.assertNotIn("ltx_viewpoint_missing", i2v["reasons"])
self.assertIn(
"ltx_conditioned_viewpoint_inherited_from_frame",
i2v["warnings"],
)
self.assertTrue(
i2v["complexity"]["observed"][
"conditioned_viewpoint_inherited_from_frame"
]
)
t2v_report = self.contract.resolve_ltx25_generation_mode(
"text_to_video",
{},
0,
)
t2v = self.contract.validate_ltx25_prompt(caption, t2v_report, {}, 5.0)
self.assertIn("ltx_viewpoint_missing", t2v["reasons"])
def test_exact_first_frame_viewpoint_anchors_are_i2v_only(self) -> None:
source_relative_prompts = (
(
"A static medium close-up preserves the exact supplied first-frame "
"camera viewpoint while the officer watches the road."
),
(
"A medium shot preserves the same supplied first-frame angle as the "
"camera makes one gentle push-in toward the officer."
),
(
"A medium close-up holds on the officer. The camera starts from the "
"exact supplied first-frame framing and remains static."
),
)
for prompt in source_relative_prompts:
with self.subTest(prompt=prompt, mode="image_to_video"):
i2v = self.contract.analyze_ltx25_prompt_complexity(
prompt,
"image_to_video",
5.0,
)
self.assertEqual(
i2v["observed"]["conditioned_viewpoint_anchor_count"],
1,
)
self.assertNotIn("ltx_viewpoint_missing", i2v["reasons"])
with self.subTest(prompt=prompt, mode="text_to_video"):
t2v = self.contract.analyze_ltx25_prompt_complexity(
prompt,
"text_to_video",
5.0,
)
self.assertEqual(
t2v["observed"]["conditioned_viewpoint_anchor_count"],
0,
)
self.assertIn("ltx_viewpoint_missing", t2v["reasons"])
def test_medium_close_shot_is_a_valid_ltx_shot_scale(self) -> None:
live_prompt = (
"The shot opens with a medium close shot of a middle-aged male officer with short light "
"brown hair, wearing a professional black police uniform with a radio microphone clipped "
"to his shoulder. He stands on a paved roadside with a dirt lot and residential houses in "
"the far background under soft, natural daylight. The camera is static at eye level, "
"focusing on him from the chest up. The officer shifts his weight from one foot to the "
"other, his expression turning tense as he turns his head toward something just off-camera "
"to the right. He takes a single, measured step backward on the gravel, his eyes widening "
"as he tracks an unseen threat. Suddenly, a man-sized, fuzzy muppet lunges into the frame "
"from the right, colliding with the officer. The officer recoils in surprise as the "
"muppet's arms grab his uniform. The sound of gravel crunching under boots is followed by "
"a muffled fabric-like impact and the officer's grunt of surprise."
)
analysis = self.contract.analyze_ltx25_prompt_complexity(
live_prompt,
"image_to_video",
10.0,
)
self.assertEqual(analysis["observed"]["estimated_shots"], 1)
self.assertEqual(analysis["observed"]["explicit_shot_phrases"], 1)
self.assertEqual(analysis["observed"]["shot_scale_count"], 1)
self.assertNotIn("ltx_shot_scale_missing", analysis["reasons"])
vague = self.contract.analyze_ltx25_prompt_complexity(
"The camera is static at eye level in a medium close framing.",
"image_to_video",
10.0,
)
self.assertIn("ltx_shot_scale_missing", vague["reasons"])
def test_i2v_continuous_scale_evolution_has_an_explicit_opening_scale(self) -> None:
metadata = self.conditioned_metadata(first=True)
mode_report = self.nodes.resolve_ltx25_generation_mode(
"image_to_video",
metadata,
1,
)
prompt = (
"One continuous take starts as a wide eye-level shot from the exact supplied "
"first-frame camera viewpoint. The camera performs a slow dolly zoom toward the "
"subject while preserving continuous space and motion, ending in a medium-close "
"shot without a cut."
)
validation = self.nodes.validate_ltx25_prompt(
prompt,
mode_report,
metadata,
8.0,
)
self.assertNotIn("ltx_shot_scale_missing", validation["reasons"])
self.assertTrue(validation["ready"], validation["reasons"])
self.assertEqual(validation["complexity"]["observed"]["estimated_shots"], 1)
def test_i2v_missing_opening_scale_is_inherited_only_with_attached_frame(self) -> None:
metadata = self.conditioned_metadata(first=True)
mode_report = self.nodes.resolve_ltx25_generation_mode(
"image_to_video",
metadata,
1,
)
prompt = (
"One continuous take starts at eye level from the exact supplied first-frame "
"camera viewpoint. The camera slowly dollies toward the subject and ends in a "
"close-up without a cut."
)
analysis = self.contract.analyze_ltx25_prompt_complexity(
prompt,
"image_to_video",
8.0,
)
self.assertIn("ltx_shot_scale_missing", analysis["reasons"])
validation = self.nodes.validate_ltx25_prompt(
prompt,
mode_report,
metadata,
8.0,
)
self.assertNotIn("ltx_shot_scale_missing", validation["reasons"])
self.assertIn(
"ltx_conditioned_shot_scale_inherited_from_frame",
validation["warnings"],
)
self.assertTrue(
validation["complexity"]["observed"][
"conditioned_shot_scale_inherited_from_frame"
]
)
self.assertTrue(validation["ready"], validation["reasons"])
missing_metadata = self.conditioned_metadata(first=False)
missing_mode_report = self.nodes.resolve_ltx25_generation_mode(
"image_to_video",
missing_metadata,
0,
)
missing_frame = self.nodes.validate_ltx25_prompt(
prompt,
missing_mode_report,
missing_metadata,
8.0,
)
self.assertIn("ltx_i2v_first_frame_missing", missing_frame["reasons"])
self.assertIn("ltx_shot_scale_missing", missing_frame["reasons"])
self.assertNotIn(
"ltx_conditioned_shot_scale_inherited_from_frame",
missing_frame["warnings"],
)
self.assertFalse(missing_frame["ready"])
def test_i2v_wide_angle_lens_wording_inherits_the_frame_scale(self) -> None:
metadata = self.conditioned_metadata(first=True)
mode_report = self.nodes.resolve_ltx25_generation_mode(
"image_to_video",
metadata,
1,
)
prompt = (
"One continuous take opens on a wide-angle, eye-level shot from the exact supplied "
"first-frame camera viewpoint. The camera performs a slow dolly zoom toward the "
"subject and ends in a medium close-up without a cut."
)
analysis = self.contract.analyze_ltx25_prompt_complexity(
prompt,
"image_to_video",
8.0,
)
self.assertEqual(analysis["observed"]["opening_shot_scale_coverage_count"], 0)
self.assertIn("ltx_shot_scale_missing", analysis["reasons"])
validation = self.nodes.validate_ltx25_prompt(
prompt,
mode_report,
metadata,
8.0,
)
self.assertNotIn("ltx_shot_scale_missing", validation["reasons"])
self.assertIn(
"ltx_conditioned_shot_scale_inherited_from_frame",
validation["warnings"],
)
self.assertTrue(
validation["complexity"]["observed"][
"conditioned_shot_scale_inherited_from_frame"
]
)
self.assertTrue(validation["ready"], validation["reasons"])
def test_wide_comma_eye_level_shot_is_an_explicit_opening_scale(self) -> None:
analysis = self.contract.analyze_ltx25_prompt_complexity(
(
"A wide, eye-level shot opens on the subject as the camera slowly dollies "
"forward through one continuous take."
),
"image_to_video",
8.0,
)
self.assertEqual(analysis["observed"]["opening_shot_scale_coverage_count"], 1)
self.assertNotIn("ltx_shot_scale_missing", analysis["reasons"])
def test_hyphenated_scale_shot_aliases_are_explicit_opening_scales(self) -> None:
cases = (
("A medium-shot at eye level", "medium shot"),
("A wide-shot at eye level", "wide shot"),
("A tighter medium-shot at eye level", "medium shot"),
)
for opening, expected_scale in cases:
with self.subTest(opening=opening):
analysis = self.contract.analyze_ltx25_prompt_complexity(
f"{opening} frames the subject while the camera remains static.",
"text_to_video",
5.0,
)
observed = analysis["observed"]
self.assertEqual(observed["shot_scale_count"], 1)
self.assertEqual(observed["opening_shot_scale_coverage_count"], 1)
self.assertEqual(
observed["shot_scale_mentions"][0]["canonical_scale"],
expected_scale,
)
self.assertNotIn("ltx_shot_scale_missing", analysis["reasons"])
def test_explicit_scale_can_precede_ots_or_pov_composition(self) -> None:
cases = (
("A wide over-the-shoulder shot", "wide shot"),
("A medium POV shot", "medium shot"),
)
for opening, expected_scale in cases:
with self.subTest(opening=opening):
analysis = self.contract.analyze_ltx25_prompt_complexity(
f"{opening} at eye level frames the subject while the camera remains static.",
"text_to_video",
5.0,
)
observed = analysis["observed"]
self.assertEqual(observed["shot_scale_count"], 1)
self.assertEqual(observed["opening_shot_scale_coverage_count"], 1)
self.assertEqual(
observed["shot_scale_mentions"][0]["canonical_scale"],
expected_scale,
)
self.assertNotIn("ltx_shot_scale_missing", analysis["reasons"])
def test_continuous_scale_destination_aliases_are_recognized(self) -> None:
for destination in (
"medium close-up",
"medium close shot",
"medium-close shot",
):
with self.subTest(destination=destination):
analysis = self.contract.analyze_ltx25_prompt_complexity(
(
"In one continuous take at eye level, the camera dollies from a wide "
f"shot into a {destination} while the subject remains centered."
),
"image_to_video",
8.0,
)
self.assertEqual(analysis["observed"]["estimated_shots"], 1)
self.assertEqual(analysis["observed"]["shot_scale_count"], 2)
self.assertNotIn("ltx_shot_scale_missing", analysis["reasons"])
def test_tighter_medium_shot_is_destination_after_medium_wide_opening(self) -> None:
metadata = self.conditioned_metadata(first=True)
mode_report = self.nodes.resolve_ltx25_generation_mode(
"image_to_video",
metadata,
1,
)
prompt = (
"A medium-wide shot at eye level frames the subject while the camera slowly dollies "
"forward in one continuous take. The shot ends in a tighter medium-shot at eye level."
)
validation = self.nodes.validate_ltx25_prompt(
prompt,
mode_report,
metadata,
8.0,
)
observed = validation["complexity"]["observed"]
self.assertEqual(observed["shot_scale_count"], 2)
self.assertEqual(
[
(item["canonical_scale"], item["role"])
for item in observed["shot_scale_mentions"]
],
[("medium-wide shot", "opening"), ("medium shot", "destination")],
)
self.assertEqual(observed["opening_shot_scale_coverage_count"], 1)
self.assertEqual(observed["opening_shot_scale_shot_indices"], [1])
self.assertNotIn("ltx_shot_scale_missing", validation["reasons"])
self.assertTrue(validation["ready"], validation["reasons"])
def test_scale_mentions_before_a_cut_do_not_cover_the_next_shot(self) -> None:
mode_report = self.nodes.resolve_ltx25_generation_mode(
"text_to_video",
{},
0,
)
prompt = (
"A wide shot at eye level holds as the camera slowly pushes into a close-up of a "
"runner. A hard cut reveals the street at eye level. The camera remains static, "
"but the second shot has no opening scale."
)
validation = self.nodes.validate_ltx25_prompt(
prompt,
mode_report,
{},
8.0,
)
self.assertEqual(validation["complexity"]["observed"]["estimated_shots"], 2)
self.assertIn("ltx_shot_scale_missing", validation["reasons"])
self.assertFalse(validation["ready"])
def test_composition_type_alone_does_not_satisfy_shot_scale(self) -> None:
for composition in (
"An over-the-shoulder shot",
"A POV",
"An insert shot",
):
with self.subTest(composition=composition):
analysis = self.contract.analyze_ltx25_prompt_complexity(
f"{composition} at eye level remains static on the subject.",
"text_to_video",
5.0,
)
self.assertEqual(analysis["observed"]["shot_scale_count"], 0)
self.assertIn("ltx_shot_scale_missing", analysis["reasons"])
def test_editorial_category_after_cut_counts_as_destination_not_scale(self) -> None:
for destination in (
"a POV of the street",
"an over-the-shoulder shot of the street",
"an insert shot of a street sign",
):
with self.subTest(destination=destination):
analysis = self.contract.analyze_ltx25_prompt_complexity(
(
"A wide shot at eye level holds while the camera remains static. "
f"A cut reveals {destination} at eye level while the camera remains static."
),
"text_to_video",
8.0,
)
observed = analysis["observed"]
self.assertEqual(observed["cut_count"], 1)
self.assertEqual(observed["estimated_shots"], 2)
self.assertEqual(observed["shot_scale_count"], 1)
self.assertEqual(observed["opening_shot_scale_shot_indices"], [1])
self.assertEqual(
observed["missing_opening_shot_scale_shot_indices"],
[2],
)
self.assertIn("ltx_shot_scale_missing", analysis["reasons"])
def test_truncated_ltx_packet_remains_salvage_only_and_fails_closed(self) -> None:
context = self.nodes.GemmaContext(
user_prompt="Continue naturally from the supplied first frame.",
source="image",
media_metadata=self.conditioned_metadata(first=True),
)
target = self.nodes.TargetProfileConfig(
target_profile="ltx",
ltx_generation_mode="image_to_video",
)
raw = (
'<|final_json|>{"ltx_prompt":"A static medium close-up at eye level holds on '
'the officer.","metadata":{"controls":{"ideogram_'
)
packet, parse_valid, warning = self.nodes.repair_or_salvage_prompt_packet(
raw, context, target
)
self.assertFalse(parse_valid)
self.assertTrue(warning)
self.assertTrue(packet["metadata"]["plain_text_salvage"])
self.assertTrue(packet["metadata"]["template_used_for_missing_fields"])
packet["metadata"].update(
{
"json_parse_valid": False,
"salvage_warning": warning,
}
)
ready, reasons = self.nodes._packet_generation_readiness(
packet,
context,
self.nodes._target_profile_config_to_dict(target),
packet["ltx_prompt"],
context.media_metadata,
8000,
)
self.assertFalse(ready)
self.assertTrue(
{"json_parse_invalid", "salvaged_output", "template_filled_missing_fields"}
.issubset(set(reasons))
)
def test_opening_conflict_detects_time_of_day_and_framing_contradictions(self) -> None:
ledger = self.frame_ledger(
(["identity", "appearance"], "A woman with red hair fills the frame."),
(["environment", "lighting"], "Bright daylight illuminates the outdoor street."),
(["camera", "composition"], "The source is a tight close-up."),
)
metadata = self.conditioned_metadata(first=True, ledger=ledger)
mode_report = self.nodes.resolve_ltx25_generation_mode("image_to_video", metadata, 1)
validation = self.nodes.validate_ltx25_prompt(
"At night, a wide shot shows the woman beneath moonlight. She remains still.",
mode_report,
metadata,
8.0,
)
self.assertIn("ltx_first_frame_prompt_conflict", validation["reasons"])
self.assertIn("daylight_to_night", validation["first_frame_conflicts"])
self.assertIn("closeup_to_wide", validation["first_frame_conflicts"])
def test_continuous_scale_destination_is_not_an_opening_frame_conflict(self) -> None:
cases = (
(
"wide_to_closeup_same_sentence",
"A wide shot frames the woman at eye level.",
(
"A wide shot at eye level frames the woman while the camera slowly dollies "
"inward, ending in a close-up without a cut."
),
),
(
"wide_to_closeup_second_sentence",
"A wide shot frames the woman at eye level.",
(
"A wide shot at eye level frames the woman. The camera slowly dollies inward "
"and ends in a close-up without a cut."
),
),
(
"closeup_to_wide_same_sentence",
"A tight close-up frames the woman at eye level.",
(
"A close-up at eye level frames the woman while the camera slowly pulls back, "
"ending in a wide shot without a cut."
),
),
(
"closeup_to_wide_second_sentence",
"A tight close-up frames the woman at eye level.",
(
"A close-up at eye level frames the woman. The camera slowly pulls back and "
"ends in a wide shot without a cut."
),
),
)
for name, source_composition, prompt in cases:
with self.subTest(case=name):
ledger = self.frame_ledger(
(["identity", "appearance"], "A woman with red hair is visible."),
(["environment", "lighting"], "She stands in a daylight studio."),
(["camera", "composition"], source_composition),
)
metadata = self.conditioned_metadata(first=True, ledger=ledger)
mode_report = self.nodes.resolve_ltx25_generation_mode(
"image_to_video",
metadata,
1,
)
validation = self.nodes.validate_ltx25_prompt(
prompt,
mode_report,
metadata,
8.0,
)
self.assertNotIn(
"ltx_first_frame_prompt_conflict",
validation["reasons"],
)
self.assertEqual(validation["first_frame_conflicts"], [])
self.assertTrue(validation["ready"], validation["reasons"])
def test_genuine_opening_scale_conflict_still_blocks_i2v(self) -> None:
cases = (
(
"wide_source_closeup_opening",
"A wide shot frames the woman at eye level.",
"A static close-up at eye level holds on the woman.",
"wide_to_closeup",
),
(
"closeup_source_wide_opening",
"A tight close-up frames the woman at eye level.",
"A static wide shot at eye level holds on the woman.",
"closeup_to_wide",
),
)
for name, source_composition, prompt, expected_conflict in cases:
with self.subTest(case=name):
ledger = self.frame_ledger(
(["identity", "appearance"], "A woman with red hair is visible."),
(["environment", "lighting"], "She stands in a daylight studio."),
(["camera", "composition"], source_composition),
)
metadata = self.conditioned_metadata(first=True, ledger=ledger)
mode_report = self.nodes.resolve_ltx25_generation_mode(
"image_to_video",
metadata,
1,
)
validation = self.nodes.validate_ltx25_prompt(
prompt,
mode_report,
metadata,
8.0,
)
self.assertIn(
"ltx_first_frame_prompt_conflict",
validation["reasons"],
)
self.assertIn(expected_conflict, validation["first_frame_conflicts"])
self.assertFalse(validation["ready"])
def test_source_scale_conflicts_use_only_camera_composition_claims(self) -> None:
ledger = self.frame_ledger(
(
["appearance", "object"],
"A close-up portrait is printed on her shirt.",
),
(["environment", "lighting"], "She stands in a daylight studio."),
(["camera", "composition"], "The source is a wide shot at eye level."),
)
metadata = self.conditioned_metadata(first=True, ledger=ledger)
mode_report = self.nodes.resolve_ltx25_generation_mode(
"image_to_video",
metadata,
1,
)
matching = self.nodes.validate_ltx25_prompt(
"A static wide shot at eye level frames the woman wearing the printed shirt.",
mode_report,
metadata,
8.0,
)
self.assertNotIn("ltx_first_frame_prompt_conflict", matching["reasons"])
self.assertEqual(matching["first_frame_conflicts"], [])
self.assertTrue(matching["ready"], matching["reasons"])
conflicting = self.nodes.validate_ltx25_prompt(
"A static close-up at eye level frames the woman wearing the printed shirt.",
mode_report,
metadata,
8.0,
)
self.assertIn("ltx_first_frame_prompt_conflict", conflicting["reasons"])
self.assertIn("wide_to_closeup", conflicting["first_frame_conflicts"])
self.assertFalse(conflicting["ready"])
def test_object_modifier_closeup_does_not_override_prompt_opening_scale(self) -> None:
ledger = self.frame_ledger(
(["identity", "appearance"], "A woman wearing a printed shirt is visible."),
(["environment", "lighting"], "She stands in a daylight studio."),
(["camera", "composition"], "The source is a wide shot at eye level."),
)
metadata = self.conditioned_metadata(first=True, ledger=ledger)
mode_report = self.nodes.resolve_ltx25_generation_mode(
"image_to_video",
metadata,
1,
)
prompt = (
"A close-up portrait is printed on her shirt as a wide shot frames her at eye "
"level while the camera remains static."
)
analysis = self.contract.analyze_ltx25_prompt_complexity(
prompt,
"image_to_video",
8.0,
)
observed = analysis["observed"]
self.assertEqual(observed["shot_scale_count"], 1)
self.assertEqual(observed["opening_shot_scale_coverage_count"], 1)
self.assertEqual(
[
(item["canonical_scale"], item["role"])
for item in observed["shot_scale_mentions"]
],
[("wide shot", "opening")],
)
self.assertEqual(
[
(item["canonical_scale"], item["role"])
for item in observed["ignored_shot_scale_mentions"]
],
[("close-up", "object_modifier")],
)
self.assertEqual(
[
item["canonical_scale"]
for item in observed["shot_scale_mentions"]
if item["covers_opening"]
],
["wide shot"],
)
validation = self.nodes.validate_ltx25_prompt(
prompt,
mode_report,
metadata,
8.0,
)
self.assertNotIn("ltx_first_frame_prompt_conflict", validation["reasons"])
self.assertEqual(validation["first_frame_conflicts"], [])
self.assertTrue(validation["ready"], validation["reasons"])
def test_first_frame_grounding_coverage_accepts_multiple_fact_families(self) -> None:
complete_ledger = self.frame_ledger(
(["identity", "appearance"], "A red-haired woman wears a dark jacket."),
(["environment", "lighting"], "She stands in a sunlit exterior courtyard."),
(["camera", "composition"], "A static medium shot frames her at eye level."),
)
complete_metadata = self.conditioned_metadata(first=True, ledger=complete_ledger)
complete_mode = self.nodes.resolve_ltx25_generation_mode(
"image_to_video", complete_metadata, 1
)
complete = self.nodes.validate_ltx25_prompt(
(
"A static medium shot at eye level holds on the red-haired woman in her dark jacket "
"inside the sunlit exterior courtyard. She slowly turns toward the doorway."
),
complete_mode,
complete_metadata,
8.0,
)
coverage = complete["first_frame_grounding"]
self.assertEqual(coverage["status"], "verified")
self.assertEqual(set(coverage["families"]), {"subject", "setting_light", "composition"})
self.assertNotIn("ltx_first_frame_grounding_incomplete", complete["reasons"])
incomplete_ledger = self.frame_ledger(
(["identity"], "A woman is visible."),
)
incomplete_metadata = self.conditioned_metadata(first=True, ledger=incomplete_ledger)
incomplete_mode = self.nodes.resolve_ltx25_generation_mode(
"image_to_video", incomplete_metadata, 1
)
incomplete = self.nodes.validate_ltx25_prompt(
"A static medium shot holds on the woman as she turns her head.",
incomplete_mode,
incomplete_metadata,
8.0,
)
self.assertEqual(incomplete["first_frame_grounding"]["status"], "incomplete")
self.assertIn("ltx_first_frame_grounding_incomplete", incomplete["reasons"])
def test_generation_gate_translates_contract_reasons_into_actionable_language(self) -> None:
metadata = {
"blocked_reasons": ["ltx_shot_count_exceeds_duration_budget"],
"ltx25_contract": {
"complexity": {
"observed": {"estimated_shots": 3},
"budget": {"max_shots": 1},
}
},
}
with self.assertRaises(ValueError) as caught:
self.nodes.DiffusionGemmaGenerationGate().gate(
"A prompt that should not pass.",
False,
json.dumps(metadata),
)
message = str(caught.exception)
self.assertIn("more shots than fit the selected duration", message)
self.assertIn("Detected 3; limit 1", message)
self.assertIn("Remove cuts or increase duration", message)
self.assertNotIn("ltx_shot_count_exceeds_duration_budget", message)
def test_splitter_explicit_resolution_ratio_controls_dimensions_preset_and_metadata(self) -> None:
context = self.nodes.GemmaContext(
user_prompt="A woman raises one hand in a quiet studio.",
source="none",
media_metadata={"source": "none"},
)
target = self.nodes.TargetProfileConfig(target_profile="ltx")
result = self.nodes.DiffusionGemmaJSONSplitter().split(
self.prompt_packet(
ltx_prompt=(
"A static medium shot at eye level holds on a woman in a quiet studio. "
"She slowly raises one hand while the locked camera remains still."
)
),
context,
target,
"",
0.5,
32,
"16:9 (Widescreen)",
)
metadata = json.loads(result[4])
self.assertEqual(result[3], "16:9")
self.assertEqual(result[9], "16:9 (Widescreen)")
self.assertEqual(result[10:12], (960, 544))
self.assertEqual(metadata["aspect_ratio"], "16:9")
self.assertEqual(metadata["resolution_selector_preset"], "16:9 (Widescreen)")
self.assertEqual(metadata["resolution_width"], 960)
self.assertEqual(metadata["resolution_height"], 544)
self.assertEqual(metadata["resolution_aspect_ratio_requested"], "16:9 (Widescreen)")
self.assertEqual(metadata["resolution_aspect_ratio_source"], "splitter_widget")
def test_splitter_auto_i2v_uses_first_frame_ratio_with_legacy_positional_call(self) -> None:
media_metadata = self.conditioned_metadata(first=True)
media_metadata.update(
{
"ltx_first_frame_width": 1080,
"ltx_first_frame_height": 1920,
}
)
context = self.nodes.GemmaContext(
user_prompt="Continue naturally from the supplied portrait first frame.",
source="image",
media_metadata=media_metadata,
)
target = self.nodes.TargetProfileConfig(
target_profile="ltx",
ltx_generation_mode="image_to_video",
)
# This is the complete positional splitter call from before
# resolution_aspect_ratio was appended. The omitted value must still
# bind to the new Auto default instead of shifting an older widget.
result = self.nodes.DiffusionGemmaJSONSplitter().split(
self.prompt_packet(
ltx_prompt=(
"The supplied portrait first-frame viewpoint holds in a static medium shot at eye level. "
"The subject slowly turns her head in one continuous take."
)
),
context,
target,
"",
0.5,
32,
)
metadata = json.loads(result[4])
self.assertEqual(result[3], "9:16")
self.assertEqual(result[9], "9:16 (Portrait Widescreen)")
self.assertEqual(result[10:12], (544, 960))
self.assertEqual(metadata["resolution_aspect_ratio_requested"], self.nodes.RESOLUTION_SELECTOR_AUTO)
self.assertEqual(metadata["resolution_aspect_ratio_source"], "ltx_first_frame")
def test_splitter_auto_ideogram_preserves_target_profile_aspect_ratio(self) -> None:
context = self.nodes.GemmaContext(
user_prompt="Create an editorial portrait poster.",
source="none",
media_metadata={"source": "none"},
)
target = self.nodes.TargetProfileConfig(
target_profile="ideogram4",
ideogram_aspect_ratio="3:4",
ideogram_json_output=False,
)
result = self.nodes.DiffusionGemmaJSONSplitter().split(
self.prompt_packet(
ideogram_prompt=(
"An editorial portrait poster with a centered subject, restrained typography, "
"soft studio lighting, and a muted blue palette."
)
),
context,
target,
resolution_megapixels=0.5,
resolution_multiple=32,
)
metadata = json.loads(result[4])
self.assertEqual(result[3], "3:4")
self.assertEqual(result[9], "3:4 (Portrait Standard)")
self.assertEqual(metadata["resolution_aspect_ratio_source"], "ideogram_target_profile")
self.assertEqual(metadata["resolution_aspect_ratio_requested"], self.nodes.RESOLUTION_SELECTOR_AUTO)
def test_splitter_resolution_selector_is_appended_after_legacy_optional_inputs(self) -> None:
optional = self.nodes.DiffusionGemmaJSONSplitter.INPUT_TYPES()["optional"]
self.assertEqual(
list(optional),
[
"gemma_context",
"target_profile_config",
"fallback_prompt",
"resolution_megapixels",
"resolution_multiple",
"resolution_aspect_ratio",
"resolution_aspect_ratio_override",
],
)
choices, config = optional["resolution_aspect_ratio"]
self.assertEqual(choices, self.nodes.RESOLUTION_SELECTOR_ASPECT_CHOICES)
self.assertEqual(config["default"], self.nodes.RESOLUTION_SELECTOR_AUTO)
override_type, override_config = optional["resolution_aspect_ratio_override"]
self.assertEqual(override_type, "STRING")
self.assertTrue(override_config["forceInput"])
def test_splitter_project_aspect_override_is_authoritative(self) -> None:
context = self.nodes.GemmaContext(
user_prompt="Create a portrait music video.",
source="none",
media_metadata={"source": "none"},
)
target = self.nodes.TargetProfileConfig(target_profile="minimax_h3")
result = self.nodes.DiffusionGemmaJSONSplitter().split(
self.prompt_packet(),
context,
target,
resolution_megapixels=0.4,
resolution_multiple=32,
resolution_aspect_ratio="16:9 (Widescreen)",
resolution_aspect_ratio_override="9:16",
)
metadata = json.loads(result[4])
self.assertEqual(result[3], "9:16")
self.assertEqual(metadata["resolution_aspect_ratio_requested"], "9:16")
self.assertEqual(
metadata["resolution_aspect_ratio_source"], "project_master_contract"
)
self.assertLess(result[10], result[11])
def test_splitter_rejects_invalid_project_aspect_override(self) -> None:
context = self.nodes.GemmaContext(
user_prompt="Create a music video.",
source="none",
media_metadata={"source": "none"},
)
target = self.nodes.TargetProfileConfig(target_profile="minimax_h3")
with self.assertRaisesRegex(ValueError, "supported canonical W:H ratios"):
self.nodes.DiffusionGemmaJSONSplitter().split(
self.prompt_packet(),
context,
target,
resolution_aspect_ratio_override="5:7",
)
def test_ltx_controls_are_appended_to_config_and_node_positional_signatures(self) -> None:
legacy_values = (
"minimax_h3",
"explicit_sound_design",
"measured footsteps",
12.0,
"cinematic",
"16:9",
"photo",
"TITLE",
False,
"custom",
"no blur",
"ref2va",
"3",
"required",
1,
"One short line.",
)
direct = self.nodes.TargetProfileConfig(*legacy_values)
self.assertEqual(direct.minimax_h3_dialogue_guidance, "One short line.")
self.assertEqual(direct.ltx_generation_mode, "auto")
self.assertEqual(direct.ltx_long_horizon_mode, "off")
self.assertEqual(direct.ltx_camera_capability, "stable")
made = self.nodes._make_target_profile_config(*legacy_values)
self.assertEqual(made.minimax_h3_dialogue_guidance, "One short line.")
self.assertEqual(made.ltx_generation_mode, "auto")
self.assertEqual(made.ltx_long_horizon_mode, "off")
self.assertEqual(made.ltx_camera_capability, "stable")
optional = self.nodes.DiffusionGemmaTargetProfile.INPUT_TYPES()["optional"]
self.assertEqual(
list(optional),
[
"minimax_h3_mode",
"minimax_h3_shot_count",
"minimax_h3_custom_shot_count",
"minimax_h3_dialogue_mode",
"minimax_h3_dialogue_line_count",
"minimax_h3_dialogue_guidance",
"ltx_generation_mode",
"ltx_long_horizon_mode",
"ltx_camera_capability",
],
)
choices, config = optional["ltx_generation_mode"]
self.assertEqual(
choices,
["Auto (recommended)", "Text to video", "Image to video", "First + last frame"],
)
self.assertEqual(config["default"], "Auto (recommended)")
long_choices, long_config = optional["ltx_long_horizon_mode"]
self.assertEqual(long_choices, ["Off", "Auto (>20 seconds)", "On"])
self.assertEqual(long_config["default"], "Off")
camera_choices, camera_config = optional["ltx_camera_capability"]
self.assertEqual(
camera_choices,
["Stable / base model", "Advanced / controlled camera"],
)
self.assertEqual(camera_config["default"], "Stable / base model")
required = (
"ltx",
"auto_scene_audio",
"",
8.0,
"",
"16:9",
"",
"",
True,
"auto",
"",
)
# Six legacy optional values end at dialogue guidance. The newly added
# Appended LTX controls must retain their defaults instead of shifting
# old widgets.
legacy_node_config, legacy_json = self.nodes.DiffusionGemmaTargetProfile().build(
*required,
"t2va",
"auto",
12,
"off",
2,
"",
)
self.assertEqual(legacy_node_config.ltx_generation_mode, "auto")
self.assertEqual(legacy_node_config.ltx_long_horizon_mode, "off")
self.assertEqual(legacy_node_config.ltx_camera_capability, "stable")
self.assertEqual(json.loads(legacy_json)["ltx_generation_mode"], "auto")
self.assertEqual(json.loads(legacy_json)["ltx_long_horizon_mode"], "off")
self.assertEqual(json.loads(legacy_json)["ltx_camera_capability"], "stable")
explicit_node_config, _json = self.nodes.DiffusionGemmaTargetProfile().build(
*required,
"t2va",
"auto",
12,
"off",
2,
"",
"First + last frame",
)
self.assertEqual(explicit_node_config.ltx_generation_mode, "first_last_frame")
self.assertEqual(explicit_node_config.ltx_long_horizon_mode, "off")
long_node_config, _json = self.nodes.DiffusionGemmaTargetProfile().build(
*required,
"t2va",
"auto",
12,
"off",
2,
"",
"Image to video",
"Auto (>20 seconds)",
)
self.assertEqual(long_node_config.ltx_generation_mode, "image_to_video")
self.assertEqual(long_node_config.ltx_long_horizon_mode, "auto")
advanced_node_config, advanced_json = self.nodes.DiffusionGemmaTargetProfile().build(
*required,
"t2va",
"auto",
12,
"off",
2,
"",
"Image to video",
"Off",
"Advanced / controlled camera",
)
self.assertEqual(advanced_node_config.ltx_camera_capability, "advanced")
self.assertEqual(json.loads(advanced_json)["ltx_camera_capability"], "advanced")
if __name__ == "__main__":
unittest.main()