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

411 lines
20 KiB
Python

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]
def load_nodes_module():
if str(COMFY_ROOT) not in sys.path:
sys.path.insert(0, str(COMFY_ROOT))
package_name = f"diffusiongemma_camera_capability_{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 LTX25CameraCapabilityTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.nodes = load_nodes_module()
cls.contract = sys.modules[cls.nodes.__package__ + ".ltx25_contract"]
cls.mode_report = {
"resolved_mode": "text_to_video",
"first_frame_attached": False,
"last_frame_attached": False,
"warnings": [],
}
def validate(
self,
prompt: str,
capability: str = "stable",
duration: float = 12.0,
long_horizon_mode: str = "off",
) -> dict:
return self.contract.validate_ltx25_prompt(
prompt,
self.mode_report,
{},
duration,
long_horizon_mode,
capability,
)
def test_target_profile_missing_field_normalizes_to_stable(self) -> None:
normalized = self.nodes._target_profile_config_to_dict(
{
"target_profile": "ltx",
"ltx_generation_mode": "Image to video",
}
)
self.assertEqual(normalized["ltx_camera_capability"], "stable")
config = self.nodes._make_target_profile_config(target_profile="ltx")
self.assertEqual(config.ltx_camera_capability, "stable")
payload = json.loads(self.nodes._target_profile_node_result(config)[1])
self.assertEqual(payload["ltx_camera_capability"], "stable")
def test_ltx_target_profile_defaults_stable_and_requires_explicit_advanced_opt_in(self) -> None:
node = self.nodes.DiffusionGemmaLTX25TargetProfile()
stable, stable_json = node.build(
generation_mode="Image to video",
target_duration_seconds=25.0,
style_guidance="",
audio_mode="auto_scene_audio",
audio_guidance="",
negative_prompt_mode="auto",
negative_prompt_guidance="",
long_horizon_mode="On",
)
advanced, advanced_json = node.build(
generation_mode="Image to video",
target_duration_seconds=25.0,
style_guidance="",
audio_mode="auto_scene_audio",
audio_guidance="",
negative_prompt_mode="auto",
negative_prompt_guidance="",
long_horizon_mode="On",
camera_capability="Advanced / controlled camera",
)
self.assertEqual(stable.ltx_camera_capability, "stable")
self.assertEqual(json.loads(stable_json)["ltx_camera_capability"], "stable")
self.assertEqual(advanced.ltx_camera_capability, "advanced")
self.assertEqual(json.loads(advanced_json)["ltx_camera_capability"], "advanced")
def test_stable_compiler_removes_ambitious_unspecified_camera_policy(self) -> None:
stable = self.contract.ltx25_compiler_contract(
"image_to_video",
25.0,
long_horizon_mode="on",
camera_capability="stable",
).casefold()
advanced = self.contract.ltx25_compiler_contract(
"image_to_video",
25.0,
long_horizon_mode="on",
camera_capability="advanced",
).casefold()
self.assertIn("stable / base model", stable)
self.assertIn("one restrained single-axis move", stable)
self.assertIn("rather than sustaining camera travel", stable)
self.assertNotIn("fill time with sustained movement, parallax", stable)
self.assertIn("one dominant physically continuous camera intention", advanced)
self.assertIn("fill time with sustained movement, parallax", advanced)
def test_stable_creativity_keeps_art_direction_but_caps_camera_ambition(self) -> None:
for mode in ("editorial", "cinematic", "concept_art", "wild"):
instruction = self.nodes._ltx_creativity_instruction(
mode,
1.0,
"image_to_video",
"stable",
).casefold()
with self.subTest(mode=mode):
self.assertIn(mode.replace("_", "-"), instruction)
self.assertIn("stable / base model", instruction)
self.assertIn("one restrained single-axis move", instruction)
self.assertIn("camera swirl", instruction)
self.assertNotIn("sweeping arc or orbit", instruction)
advanced = self.nodes._ltx_creativity_instruction(
"wild",
1.0,
"image_to_video",
"advanced",
).casefold()
self.assertIn("sweeping arc or orbit", advanced)
self.assertIn("compound camera", advanced)
def test_stable_validation_rejects_each_high_risk_camera_family(self) -> None:
cases = {
"ltx_stable_camera_orbit_or_rotation": [
"The camera begins a slow dramatic circular orbit around the subject.",
"The camera slows its rotation.",
"The camera spins around her.",
"The camera swirls around her.",
],
"ltx_stable_camera_sweeping_or_whip_motion": [
"The camera makes a sweeping move around the performer.",
"The camera performs a whip-pan to the right.",
],
"ltx_stable_camera_dolly_zoom": [
"The camera performs a dolly zoom toward her face.",
],
"ltx_stable_camera_shake_or_handheld_pursuit": [
"The camera shakes violently beside the runner.",
"A handheld pursuit follows the runner down the alley.",
],
"ltx_stable_camera_compound_or_reversal": [
"The camera uses a compound multi-axis path around the performer.",
"The camera pushes in and then pulls back before the end.",
],
"ltx_stable_camera_pronounced_parallax": [
"The shot creates dizzying layered parallax around the subject.",
],
}
for expected_reason, clauses in cases.items():
for clause in clauses:
prompt = f"A medium shot at eye level frames a performer. {clause}"
with self.subTest(reason=expected_reason, clause=clause):
stable = self.validate(prompt, "stable")
advanced = self.validate(prompt, "advanced")
self.assertIn(expected_reason, stable["reasons"])
self.assertNotIn(expected_reason, advanced["reasons"])
def test_user_nightmare_prompt_is_hard_failure_only_in_stable_mode(self) -> None:
prompt = (
"A medium shot at eye level frames the performer. Through most of the shot, "
"the camera continues its sweeping arc, creating a dizzying parallax effect "
"with the bills that flutter occasionally as if caught in a faint draft. "
"Near the end, the camera slows its rotation and settles on her face."
)
stable = self.validate(prompt, "stable", 25.0, "on")
advanced = self.validate(prompt, "advanced", 25.0, "on")
self.assertFalse(stable["ready"])
self.assertIn("ltx_stable_camera_orbit_or_rotation", stable["reasons"])
self.assertIn("ltx_stable_camera_sweeping_or_whip_motion", stable["reasons"])
self.assertIn("ltx_stable_camera_pronounced_parallax", stable["reasons"])
self.assertIn("ltx_stable_camera_sustained_travel", stable["reasons"])
self.assertFalse(
any(reason.startswith("ltx_stable_camera_") for reason in advanced["reasons"])
)
def test_negated_risky_language_is_not_treated_as_positive_camera_direction(self) -> None:
prompt = (
"A medium shot at eye level frames the performer while the camera remains locked "
"and static. No sweeping arc, whip-pan, dolly zoom, camera shake, handheld pursuit, "
"circular orbit, rotation, spin, swirl, roll, compound multi-axis movement, direction "
"reversal, pronounced layered dizzying parallax, or sustained camera travel is used."
)
report = self.validate(prompt, "stable", 25.0, "on")
self.assertFalse(
any(reason.startswith("ltx_stable_camera_") for reason in report["reasons"]),
report,
)
def test_subject_swirl_is_not_misclassified_as_camera_swirl(self) -> None:
prompt = (
"A medium shot at eye level frames a dancer while the camera remains locked and "
"static. Her skirt swirls around her ankles as she turns once."
)
report = self.validate(prompt, "stable")
self.assertNotIn("ltx_stable_camera_orbit_or_rotation", report["reasons"])
def test_subject_and_environment_motion_do_not_bind_to_stable_camera(self) -> None:
safe_prompts = (
"A medium shot at eye level frames her; the camera stays static while coins circle around the performer.",
"A medium shot at eye level frames her; the camera is stabilized as ribbons orbit the dancer.",
"A medium shot at eye level frames her; the camera is locked while a sweeping curtain crosses the background.",
"A medium shot at eye level frames her; the camera is locked as the performer begins a sweeping arc with her arm.",
"A medium shot at eye level frames her; the shot stays steady while the dancer performs a sweeping arc.",
"A medium shot at eye level frames her; the camera is locked while the dancer performs an orbit around the stage.",
"A medium shot at eye level frames her; the camera is locked while the dancer moves throughout.",
"A static medium shot at eye level frames her while the performer moves continuously throughout.",
"A medium shot at eye level holds with framing fixed while bills travel throughout the background.",
"A medium shot at eye level holds; the camera is locked while fog moves for the duration.",
)
for prompt in safe_prompts:
with self.subTest(prompt=prompt):
report = self.validate(prompt, "stable", 25.0, "on")
self.assertFalse(
any(
reason.startswith("ltx_stable_camera_")
for reason in report["reasons"]
),
report,
)
def test_negating_a_safe_hold_does_not_negate_the_risky_move_that_follows(self) -> None:
prompts = (
"A medium shot at eye level frames her. Without a locked camera, the shot begins a slow circular orbit.",
"A medium shot at eye level frames her. No longer locked, the camera begins a slow circular orbit.",
"A medium shot at eye level frames her. The camera does not stop as it orbits her.",
"A medium shot at eye level frames her. The camera keeps moving as it orbits her.",
"A medium shot at eye level frames her. A circular tracking arc surrounds the subject.",
)
for prompt in prompts:
with self.subTest(prompt=prompt):
self.assertIn(
"ltx_stable_camera_orbit_or_rotation",
self.validate(prompt, "stable")["reasons"],
)
def test_short_range_camera_pronouns_preserve_risky_motion_ownership(self) -> None:
risky = (
"A medium shot at eye level frames her. The camera begins locked. Then it orbits.",
"A medium shot at eye level frames her. The shot starts static. It then spins.",
"A medium shot at eye level frames her. The camera starts stable, but it soon swirls.",
"A medium shot at eye level frames her. The camera begins in a stable view. Near the end, it slows its rotation.",
"A medium shot at eye level frames her. The camera begins with a stable view. Near the end, it slows its rotation.",
"A medium shot at eye level frames her while the camera begins locked. Then it orbits around her.",
"A medium shot at eye level frames her while the camera starts static. Near the end, it spins.",
"A medium shot frames her and the camera remains stable. Then it rotates around her.",
)
for prompt in risky:
with self.subTest(prompt=prompt):
self.assertIn(
"ltx_stable_camera_orbit_or_rotation",
self.validate(prompt, "stable")["reasons"],
)
subject_pronoun = (
"A medium shot at eye level frames a robot while the camera stays static. "
"The robot lifts one hand. It spins once."
)
self.assertNotIn(
"ltx_stable_camera_orbit_or_rotation",
self.validate(subject_pronoun, "stable")["reasons"],
)
ambiguous_subject_pronouns = (
"A medium shot at eye level frames a toy top while the camera remains locked. Then it spins.",
"A medium shot at eye level frames a coin while the shot remains static. It then rotates.",
"A medium shot at eye level frames a robot while the camera remains locked on it. Then it spins once.",
)
for prompt in ambiguous_subject_pronouns:
with self.subTest(prompt=prompt):
self.assertNotIn(
"ltx_stable_camera_orbit_or_rotation",
self.validate(prompt, "stable")["reasons"],
)
def test_negated_stop_or_static_state_does_not_hide_persistent_camera_rotation(self) -> None:
prompts = (
"A medium shot at eye level frames her. The camera never stops rotating around her.",
"A medium shot at eye level frames her. The camera does not stop rotating around her.",
"A medium shot at eye level frames her. The camera never stops its orbit around her.",
"A medium shot at eye level frames her. The camera does not cease to orbit her.",
"A medium shot at eye level frames her. The camera is never static and instead orbits her.",
"A medium shot at eye level frames her. The camera does not slow its sweeping arc around her.",
)
for prompt in prompts:
with self.subTest(prompt=prompt):
self.assertIn(
"ltx_stable_camera_orbit_or_rotation",
self.validate(prompt, "stable")["reasons"],
)
def test_simple_stable_camera_states_and_single_axis_moves_pass_risk_validation(self) -> None:
clauses = (
"the camera remains locked and static",
"the stabilized camera makes one short gentle push-in and settles",
"the stabilized camera makes one short gentle pull-back and settles",
"the stabilized camera makes one small pan left and settles",
"the stabilized camera makes one small tilt up and settles",
"the stabilized camera makes one short lateral track right and settles",
)
for clause in clauses:
prompt = f"A medium shot at eye level frames a performer while {clause}."
with self.subTest(clause=clause):
report = self.validate(prompt, "stable")
self.assertFalse(
any(
reason.startswith("ltx_stable_camera_")
for reason in report["reasons"]
),
report,
)
def test_long_horizon_stable_rejects_sustained_travel_and_advanced_allows_it(self) -> None:
prompt = (
"A medium shot at eye level frames the performer as the camera tracks right "
"throughout the shot and finally settles on the same subject."
)
stable = self.validate(prompt, "stable", 25.0, "on")
advanced = self.validate(prompt, "advanced", 25.0, "on")
self.assertIn("ltx_stable_camera_sustained_travel", stable["reasons"])
self.assertNotIn("ltx_stable_camera_sustained_travel", advanced["reasons"])
def test_low_level_legacy_defaults_remain_advanced_but_target_path_is_stable(self) -> None:
prompt = (
"A medium shot at eye level frames the performer while the camera orbits her."
)
legacy = self.contract.validate_ltx25_prompt(
prompt,
self.mode_report,
{},
12.0,
)
self.assertEqual(legacy["camera_capability"], "advanced")
self.assertNotIn("ltx_stable_camera_orbit_or_rotation", legacy["reasons"])
context = self.nodes.GemmaContext(user_prompt="A performer dances.")
target = self.nodes._target_profile_config_to_dict({"target_profile": "ltx"})
diagnostics = self.nodes._ltx25_contract_diagnostics(
prompt,
context,
target,
{},
)
self.assertEqual(diagnostics["camera_capability"], "stable")
self.assertIn("ltx_stable_camera_orbit_or_rotation", diagnostics["reasons"])
def test_stable_model_prompt_has_no_unconditional_advanced_camera_authorization(self) -> None:
metadata = {
"source": "image",
"reference_image_count": 1,
"ltx_first_frame_attached": True,
"ltx_last_frame_attached": False,
}
for long_horizon_mode in ("off", "on"):
stable = self.nodes._build_model_prompt(
"A performer dances among loose bills.",
self.nodes.DEFAULT_MASTER_PROMPT,
"ltx",
metadata,
creativity_mode="wild",
creative_strength=1.0,
ltx_generation_mode="image_to_video",
ltx_long_horizon_mode=long_horizon_mode,
ltx_camera_capability="stable",
).casefold()
with self.subTest(long_horizon_mode=long_horizon_mode):
self.assertIn("camera capability is stable / base model", stable)
self.assertIn("creativity-driven camera ambition", stable)
self.assertNotIn("zooming out while rotating 180 degrees", stable)
self.assertNotIn("may author mode-appropriate camera choreography", stable)
self.assertNotIn("may be assertive, fast, or compound", stable)
self.assertNotIn("fill the long middle with sustained travel, parallax", stable)
self.assertNotIn("one dominant continuous camera-and-subject path", stable)
advanced = self.nodes._build_model_prompt(
"A performer dances among loose bills.",
self.nodes.DEFAULT_MASTER_PROMPT,
"ltx",
metadata,
creativity_mode="wild",
creative_strength=1.0,
ltx_generation_mode="image_to_video",
ltx_long_horizon_mode="on",
ltx_camera_capability="advanced",
).casefold()
self.assertIn("zooming out while rotating 180 degrees", advanced)
self.assertIn("fill the long middle with sustained travel, parallax", advanced)
if __name__ == "__main__":
unittest.main()