# Copyright (c) 2026 exportAnything. All rights reserved.
# SPDX-License-Identifier: MIT
import json
import sys
import tempfile
from pathlib import Path
from types import SimpleNamespace
from jsonschema import Draft202012Validator
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
import nodes as dg # noqa: E402
import scripts.dg_to_ltx_prompt_injector as prompt_injector # noqa: E402
def _packet(prompt: str, metadata: dict | None = None, segments: list[dict] | None = None) -> dict:
return {
"ltx_prompt": prompt,
"ideogram_prompt": "",
"negative_prompt": "blurry",
"scene_segments": segments if segments is not None else [{"index": 0, "duration_seconds": 2, "prompt": prompt}],
"metadata": metadata or {},
}
def _split(packet_or_text, context=None, target=None):
text = packet_or_text if isinstance(packet_or_text, str) else json.dumps(packet_or_text)
return dg.DiffusionGemmaJSONSplitter().split(
text,
gemma_context=context,
target_profile_config=target,
)
def _metadata(result):
return json.loads(result[4])
def _media_context(user_prompt: str, media: dict, visual_description: str = ""):
metadata = dict(media)
if visual_description:
metadata["visual_description"] = visual_description
return dg.GemmaContext(
user_prompt=user_prompt,
images=object(),
source=str(metadata.get("source", "image")),
media_metadata=metadata,
visual_description=visual_description,
warnings=[],
)
def _h3_ref_context(user_prompt: str, manifest: str):
normalized_manifest = dg._normalize_minimax_h3_reference_manifest(manifest)
definitions = dg._minimax_h3_reference_definitions(normalized_manifest)
return dg.GemmaContext(
user_prompt=user_prompt,
source="minimax_h3_ref2va",
media_metadata={
"minimax_h3_mode": "ref2va",
"minimax_h3_reference_manifest": normalized_manifest,
"minimax_h3_reference_tags": [tag for tag, _description in definitions],
"minimax_h3_reference_manifest_reasons": dg._minimax_h3_reference_manifest_validation_reasons(
normalized_manifest
),
},
)
def _assert_ready(result):
assert result[8] is True, _metadata(result)
assert result[12] is True, _metadata(result)
assert result[0], "expected executable LTX prompt"
def _assert_blocked(result, expected_reason: str):
metadata = _metadata(result)
assert result[8] is False, metadata
assert result[12] is False, metadata
assert result[0] == "", metadata
assert expected_reason in metadata.get("blocked_reasons", []), metadata
def test_readiness_gates():
ready = _split(
_packet(
"In a wide shot, the camera remains static at eye level as a red kite rises over a windy beach at sunset."
)
)
_assert_ready(ready)
segmented = _split(
_packet(
"In a medium shot, the camera remains static at eye level as two simple beats unfold.",
segments=[
{"index": 0, "duration_seconds": 2, "prompt": "First beat."},
{"index": 1, "duration_seconds": 3, "prompt": "Second beat."},
],
)
)
assert segmented[7] == "2, 3", segmented[7]
salvaged = _split("ltx_prompt: A clean plain text prompt that was not valid JSON.")
_assert_blocked(salvaged, "salvaged_output")
fallback = _split("", context=dg.GemmaContext(user_prompt="make a cinematic shot", media_metadata={}))
_assert_blocked(fallback, "template_fallback")
metadata_only_context = _media_context(
"Use this image as an LTX prompt.",
{
"source": "image",
"pixel_tensor_present": True,
"pixels_sent_to_backend": False,
},
)
metadata_only_claim = _split(_packet("A sunlit daylight courtyard with people in the background."), metadata_only_context)
_assert_blocked(metadata_only_claim, "metadata_only_media_reference_request")
night_context = _media_context(
"Use the supplied visual description.",
{
"source": "image",
"pixel_tensor_present": True,
"pixels_sent_to_backend": False,
},
"Nighttime locked-off static camera view under neon streetlights.",
)
model_media = {
"media": {
"source": "image",
"pixels_sent_to_backend": True,
"visual_description": "daylight exterior",
}
}
night_ready = _split(
_packet(
"In a wide shot from an eye-level front view, the camera remains locked and static on a nighttime neon street scene.",
model_media,
),
night_context,
)
_assert_ready(night_ready)
night_metadata = _metadata(night_ready)
assert "Nighttime" in night_metadata["media"]["visual_description"], night_metadata
assert "model_reported_media" in night_metadata, night_metadata
daylight_contradiction = _split(_packet("A bright daylight street with a blue sky.", model_media), night_context)
_assert_blocked(daylight_contradiction, "visual_description_contradiction_daylight_claim")
camera_contradiction = _split(_packet("The camera slowly zooms in on the neon street.", model_media), night_context)
_assert_blocked(camera_contradiction, "visual_description_contradiction_camera_motion")
def test_prompt_packet_schema():
schema = json.loads((ROOT / "schemas" / "prompt_packet.schema.json").read_text(encoding="utf-8"))
validator = Draft202012Validator(schema)
# Packets created before MiniMax H3 support remain valid.
valid_packet = _packet("A clean prompt.", {"ready_for_generation": True, "blocked_reasons": []})
validator.validate(valid_packet)
h3_packet = _packet("", {"ready_for_generation": True, "blocked_reasons": []}, segments=[])
h3_packet["minimax_h3_prompt"] = (
"integrated_multimodal_description: [Shot 1] A locked-off wide shot holds on a rain-darkened city roof.\n\n"
"overall_soundscape: Wind moves across the roof while distant traffic hums below.\n\n"
"non_diegetic_music: N/A"
)
validator.validate(h3_packet)
invalid_h3_packet = dict(h3_packet, minimax_h3_prompt=["not", "a", "string"])
assert list(validator.iter_errors(invalid_h3_packet)), "schema should reject a non-string H3 prompt"
invalid_packet = {"ltx_prompt": "missing required fields"}
errors = sorted(validator.iter_errors(invalid_packet), key=lambda item: item.path)
assert errors, "schema should reject incomplete prompt packets"
def test_minimax_h3_contract_and_splitter_compatibility():
profile_choices = dg.DiffusionGemmaTargetProfile.INPUT_TYPES()["required"]["target_profile"][0]
assert "minimax_h3" in profile_choices, profile_choices
target = dg._make_target_profile_config(
target_profile="minimax_h3",
target_duration_seconds=20.0,
audio_mode="auto_scene_audio",
)
model_prompt = dg._build_model_prompt(
"A runner crosses rain-darkened rooftops in four hard-cut shots.",
dg.DEFAULT_MASTER_PROMPT,
"minimax_h3",
{},
target_duration_seconds=20.0,
audio_mode="auto_scene_audio",
thinking_mode="on",
)
lowered = model_prompt.lower()
assert dg._target_profile_config_to_dict(target)["target_duration_seconds"] == 20.0
assert "minimax_h3_prompt" in model_prompt
assert "integrated_multimodal_description" in model_prompt
assert "overall_soundscape" in model_prompt
assert "non_diegetic_music" in model_prompt
assert "20" in model_prompt, "20-second H3 requests must not be clamped to the published 15-second guidance"
assert "target profile: minimax_h3" in lowered
assert "composed for 1:1" not in lowered, "the Ideogram-only aspect control must not silently frame H3"
for phrase in (
"binding directing specification",
"continuity ledger",
"generic real animals",
"prior shot's resulting position and state",
"an incoming cut, shot size, angle, subject framing",
"event-specific sound immediately beside its visible cause",
"the final frame holds on",
):
assert phrase in lowered, phrase
inferred_duration_prompt = dg._build_model_prompt(
"Make a 20-second rooftop chase.",
dg.DEFAULT_MASTER_PROMPT,
"minimax_h3",
{},
target_duration_seconds=0.0,
audio_mode="auto_scene_audio",
thinking_mode="on",
)
assert "planned for exactly 20.000 seconds" in inferred_duration_prompt, inferred_duration_prompt
h3_prompt = (
"integrated_multimodal_description: [Shot 1] A high side angle tracks a runner toward a wet rooftop edge.\n\n"
"[Shot 2] At 00:10.000, the camera cuts to a wide profile as the runner clears the gap.\n\n"
"overall_soundscape: Strong wind, rapid footsteps, and distant city traffic surround the action.\n\n"
"non_diegetic_music: A low percussion pulse builds under the chase."
)
packet = _packet("", {"ready_for_generation": True, "blocked_reasons": []}, segments=[])
packet["minimax_h3_prompt"] = h3_prompt
result = _split(packet, context=None, target=target)
assert dg.DiffusionGemmaJSONSplitter.RETURN_NAMES[:13] == (
"ltx_prompt",
"ideogram_prompt",
"negative_prompt",
"aspect_ratio",
"metadata_json",
"scene_segments_json",
"local_prompts",
"segment_lengths",
"is_valid",
"resolution_selector_preset",
"resolution_width",
"resolution_height",
"ready_for_generation",
)
assert dg.DiffusionGemmaJSONSplitter.RETURN_NAMES[13:] == (
"minimax_h3_prompt",
"candidate_ltx_prompt",
"candidate_negative_prompt",
"candidate_minimax_h3_prompt",
)
assert len(result) == 17, result
assert result[13] == h3_prompt, result[13]
assert "\n\noverall_soundscape:" in result[13], result[13]
assert "\n\nnon_diegetic_music:" in result[13], result[13]
assert result[0] == "", result[0]
assert result[1] == "", result[1]
assert result[2] == "", result[2]
assert result[16] == h3_prompt, result[16]
assert result[8] is True, _metadata(result)
assert result[12] is True, _metadata(result)
assert result[14] == "", result[14]
assert result[15] == "", result[15]
model_style_h3_prompt = (
"[Shot 1] A low-angle tracking shot follows a runner across a rain-darkened rooftop. "
"[Shot 2] At 00:04.500 the camera cuts to a wide profile as the runner clears the gap.\n\n"
"Strong wind, rapid footsteps, splashing puddles, and distant city traffic surround the action.\n\n"
"A restrained electronic score uses deep cello pulses and measured percussion, building with each leap."
)
model_style_packet = _packet("", {"ready_for_generation": True, "blocked_reasons": []}, segments=[])
model_style_packet["minimax_h3_prompt"] = model_style_h3_prompt
model_style_result = _split(model_style_packet, context=None, target=target)
normalized_h3_prompt = model_style_result[13]
assert model_style_result[12] is True, _metadata(model_style_result)
assert normalized_h3_prompt.startswith("integrated_multimodal_description: [Shot 1]"), normalized_h3_prompt
assert "[Shot 2] At 00:04.500, the camera cuts to" in normalized_h3_prompt, normalized_h3_prompt
assert "\n\noverall_soundscape: Strong wind" in normalized_h3_prompt, normalized_h3_prompt
assert "\n\nnon_diegetic_music: A restrained electronic score" in normalized_h3_prompt, normalized_h3_prompt
assert dg._normalize_minimax_h3_prompt(normalized_h3_prompt) == normalized_h3_prompt
unlabeled_cut_prompt = (
"integrated_multimodal_description: [Shot 1] A locked-off wide shot holds on a rain-darkened roof. "
"At 00:04.500, the camera cuts to a low-angle tracking shot beside the runner. "
"At 00:09.000, the camera cuts to an overhead static hold as the runner lands.\n\n"
"overall_soundscape: Wind, footsteps, and splashing water follow the action.\n\n"
"non_diegetic_music: Low percussion builds under the cuts."
)
normalized_unlabeled_cut_prompt = dg._normalize_minimax_h3_prompt(unlabeled_cut_prompt)
assert "[Shot 2] At 00:04.500," in normalized_unlabeled_cut_prompt, normalized_unlabeled_cut_prompt
assert "[Shot 3] At 00:09.000," in normalized_unlabeled_cut_prompt, normalized_unlabeled_cut_prompt
assert "minimax_h3_unlabeled_cut" not in dg._minimax_h3_prompt_validation_reasons(
normalized_unlabeled_cut_prompt,
20.0,
)
ambiguous_unlabeled_packet = _packet("", {"ready_for_generation": True, "blocked_reasons": []}, segments=[])
ambiguous_unlabeled_packet["minimax_h3_prompt"] = (
"[Shot 1] A locked-off view holds on a rooftop.\n\nA second unlabeled visual paragraph follows."
)
ambiguous_unlabeled_result = _split(ambiguous_unlabeled_packet, context=None, target=target)
assert ambiguous_unlabeled_result[13] == "", ambiguous_unlabeled_result[13]
assert "minimax_h3_missing_integrated_multimodal_description" in _metadata(ambiguous_unlabeled_result)["blocked_reasons"]
invalid_cases = (
(
h3_prompt.replace("[Shot 2] At 00:10.000,", "[10s-20s] Shot 2:"),
"minimax_h3_noncanonical_time_range",
),
(
h3_prompt.replace("At 00:10.000,", "At 00:20.000,"),
"minimax_h3_cut_timestamp_out_of_range",
),
(
h3_prompt.replace("[Shot 1]", "[Shot 1] At 00:00.000,"),
"minimax_h3_shot1_timestamp_invalid",
),
(
h3_prompt.rsplit("non_diegetic_music:", 1)[0] + "non_diegetic_music:",
"minimax_h3_empty_non_diegetic_music",
),
(
h3_prompt.replace(
"[Shot 1] A high side angle tracks a runner toward a wet rooftop edge.",
"[Shot 1]",
),
"minimax_h3_empty_visual_timeline",
),
(
h3_prompt.replace("[Shot 2]", "[shot 2]"),
"minimax_h3_shot_syntax_invalid",
),
(
h3_prompt.replace("\n\noverall_soundscape:", "\n\nCamera: handheld.\n\noverall_soundscape:"),
"minimax_h3_extra_top_level_field",
),
(
h3_prompt.replace(
"A high side angle tracks a runner toward a wet rooftop edge.",
"A high side angle tracks a runner who says [English] Jump now. near a wet rooftop edge.",
),
"minimax_h3_dialogue_speaker_invalid",
),
)
for invalid_prompt, expected_reason in invalid_cases:
invalid_packet = _packet("", {"ready_for_generation": True, "blocked_reasons": []}, segments=[])
invalid_packet["minimax_h3_prompt"] = invalid_prompt
invalid_result = _split(invalid_packet, context=None, target=target)
invalid_metadata = _metadata(invalid_result)
assert invalid_result[13] == "", invalid_result[13]
assert expected_reason in invalid_metadata["blocked_reasons"], invalid_metadata
visual_only_target = dg._make_target_profile_config(
target_profile="minimax_h3",
target_duration_seconds=20.0,
audio_mode="visual_only",
)
visual_only_packet = _packet("", {"ready_for_generation": True, "blocked_reasons": []}, segments=[])
visual_only_packet["minimax_h3_prompt"] = h3_prompt
visual_only_result = _split(visual_only_packet, context=None, target=visual_only_target)
assert visual_only_result[13] == "", visual_only_result[13]
assert "minimax_h3_visual_only_audio_fields_invalid" in _metadata(visual_only_result)["blocked_reasons"]
requested_transition_context = dg.GemmaContext(user_prompt="Use a cross-dissolve for the second shot.", media_metadata={})
requested_transition_prompt = h3_prompt.replace("the camera cuts to", "the camera cross-dissolves to")
requested_transition_packet = _packet("", {"ready_for_generation": True, "blocked_reasons": []}, segments=[])
requested_transition_packet["minimax_h3_prompt"] = requested_transition_prompt
requested_transition_result = _split(requested_transition_packet, requested_transition_context, target)
assert requested_transition_result[13] == requested_transition_prompt, _metadata(requested_transition_result)
oversized_packet = _packet("", {"ready_for_generation": True, "blocked_reasons": []}, segments=[])
oversized_packet["minimax_h3_prompt"] = h3_prompt.replace(
"A high side angle tracks a runner toward a wet rooftop edge.",
"A high side angle tracks a runner toward a wet rooftop edge. " + ("Visible rain streaks across the frame. " * 260),
)
oversized_result = _split(oversized_packet, context=None, target=target)
assert oversized_result[13] == "", oversized_result[13]
assert "minimax_h3_prompt_truncated" in _metadata(oversized_result)["blocked_reasons"]
def test_minimax_h3_ref2va_contract():
mode_input = dg.DiffusionGemmaTargetProfile.INPUT_TYPES()["optional"]["minimax_h3_mode"]
assert mode_input[0] == ["t2va", "ref2va"], mode_input
assert mode_input[1]["default"] == "t2va", mode_input
default_target = dg._make_target_profile_config(target_profile="minimax_h3", target_duration_seconds=8.0)
assert dg._target_profile_config_to_dict(default_target)["minimax_h3_mode"] == "t2va"
default_t2va_prompt = (
"integrated_multimodal_description: [Shot 1] A static wide shot holds on a quiet harbor at dawn. "
"The final frame holds on the anchored boats beneath the pale sky.\n\n"
"overall_soundscape: Small waves touch the dock while ropes creak in the breeze.\n\n"
"non_diegetic_music: N/A"
)
default_t2va_packet = _packet("", {"ready_for_generation": True, "blocked_reasons": []}, segments=[])
default_t2va_packet["minimax_h3_prompt"] = default_t2va_prompt
default_t2va_result = _split(default_t2va_packet, target=default_target)
assert default_t2va_result[13] == default_t2va_prompt, _metadata(default_t2va_result)
assert default_t2va_result[12] is True, _metadata(default_t2va_result)
manifest = (
": first-frame composition and primary identity reference for the rooftop courier; fully preserve the face, "
"short black hair, red waxed-canvas coat, silver satchel, body proportions, and opening screen position.\n"
"