From 009cd4750967d49c482610ff09decdb7b6b3a971 Mon Sep 17 00:00:00 2001 From: Darksidewalker <> Date: Sun, 16 Aug 2026 21:55:21 +0200 Subject: [PATCH] fix: serialize Director prompts --- js/minimax_h3_director.js | 22 +++++--- nodes/helper_minimax_h3_prompt_builder.py | 43 ++++++-------- nodes/nodes_minimax_h3_director.py | 6 +- ...inimax_h3_director_prompt_compatibility.py | 56 +++++-------------- 4 files changed, 50 insertions(+), 77 deletions(-) diff --git a/js/minimax_h3_director.js b/js/minimax_h3_director.js index 2043fca..d77ccc2 100644 --- a/js/minimax_h3_director.js +++ b/js/minimax_h3_director.js @@ -58,8 +58,7 @@ function builderHasContent(builder) { const ref = builder.ref; return !!(ref && typeof ref === "object" && ( ["subject_definitions", "summary", "retention_analysis", "detailed_description", "soundscape"].some(key => textValue(ref[key])) || - (Array.isArray(ref.subject_defs) && ref.subject_defs.length) || - (Array.isArray(ref.retention) && ref.retention.length) + (Array.isArray(ref.subject_defs) && ref.subject_defs.length) || (Array.isArray(ref.retention) && ref.retention.length) )); } function legacyPrompt(state, widgetPrompt) { @@ -225,7 +224,7 @@ function install(node) { })); }, { passive: false }); const setStatus = (message, isError = false) => { status.textContent = message; status.classList.toggle("error", isError); }; - const emit = () => { builderState.mode = mode(); state.builder_state = builderState; dataWidget.value = JSON.stringify(state); dataWidget.callback?.(dataWidget.value); if (builderWidget) { builderWidget.value = JSON.stringify(builderState); builderWidget.callback?.(builderWidget.value); } node.graph?.setDirtyCanvas(true, true); }; + const emit = () => { builderState.mode = mode(); const duration = Number(node.widgets?.find(w => w.name === "duration")?.value); if (Number.isFinite(duration)) builderState.duration = duration; const resolved = builderPromptForWidget(builderState, mode()); if (promptWidget && promptWidget.value !== resolved) { promptWidget.value = resolved; promptWidget.callback?.(resolved); } state.builder_state = builderState; state.resolved_prompt = resolved; dataWidget.value = JSON.stringify(state); dataWidget.callback?.(dataWidget.value); if (builderWidget) { builderWidget.value = JSON.stringify(builderState); builderWidget.callback?.(builderWidget.value); } node.graph?.setDirtyCanvas(true, true); }; const promptStyle = () => { const v = builderState?.prompt_mode; return v === "simple" || v === "structured" ? v : "structured"; }; function legacySimplePromptFor(m) { if (m === "REF2VA") { @@ -245,6 +244,18 @@ function install(node) { `non_diegetic_music: ${builderState.music || "N/A"}`, ].filter(Boolean).join("\n"); } + function builderPromptForWidget(builder, m) { + if (builder?.prompt_mode === "simple" && typeof builder.simple_prompt === "string") return builder.simple_prompt; + const r = builder?.ref || {}; + if (m === "REF2VA") return `subject_definitions:\n${r.subject_definitions || ""}\n\nsummary:\n${r.summary || ""}\n\nretention_analysis:\n${r.retention_analysis || ""}\n\ndetailed_description:\n${r.detailed_description || ""}\n\noverall_soundscape:\n${r.soundscape || ""}\n\nnon_diegetic_music:\n${r.music || "N/A"}`; + const music = builder?.music || "N/A"; + let head = ""; + if (m === "I2VA") head = "For the target video, at 0.00 seconds into the target video, (from [Shot 1]) is fully referenced."; + else if (m === "FL2VA") head = "How the reference pictures align with the target video — Picture 1 (from Shot 1) aligns with the 0.00-second mark of the target video; Picture 2 (from Shot 2) aligns with the end mark of the target video."; + else if (m === "L2VA") head = "How the reference pictures align with the target video — (from [Shot 1]) aligns with the end mark of the target video."; + const body = `integrated_multimodal_description: ${builder?.imd || ""}\n\noverall_soundscape: ${builder?.soundscape || ""}\n\nnon_diegetic_music: ${music}`; + return head ? `${head}\n\n${body}` : body; + } function previewTextFor(m, external) { if (external) { const t = String(externalPromptWidget()?.value || "").trim(); @@ -851,17 +862,14 @@ function install(node) { builderState = baseDefaults; builderState.mode = m; } - // Workflows created before the builder stored complex text in the ordinary - // prompt widget/timeline payload. Keep that exact text editable and queue - // it unchanged instead of replacing it with blank structured fields. if (!builderHasContent(builderState)) { const preserved = legacyPrompt(state, promptWidget?.value); if (preserved) { builderState.prompt_mode = "simple"; builderState.simple_prompt = preserved; - emit(); } } + emit(); syncNodeBounds(); render(); void Promise.all(state.items.filter(item => item.type === "video" && !item.thumbnail).map(async item => { diff --git a/nodes/helper_minimax_h3_prompt_builder.py b/nodes/helper_minimax_h3_prompt_builder.py index e9ec84e..2bd2a7c 100644 --- a/nodes/helper_minimax_h3_prompt_builder.py +++ b/nodes/helper_minimax_h3_prompt_builder.py @@ -250,11 +250,7 @@ def _build_simple_prompt(state: dict) -> str: def has_builder_content(state: dict) -> bool: - """Return whether a builder state contains an authored prompt. - - Empty default fields (including the assembly-time ``N/A`` music fallback) - must not prevent a pre-builder Director prompt from being migrated. - """ + """Return whether a builder state contains authored prompt text.""" if not isinstance(state, dict): return False if _ensure_str(state.get("simple_prompt")): @@ -262,28 +258,19 @@ def has_builder_content(state: dict) -> bool: if _ensure_str(state.get("imd")) or _ensure_str(state.get("soundscape")): return True ref = state.get("ref") - if not isinstance(ref, dict): - return False - return any(_ensure_str(ref.get(key)) for key in ( - "subject_definitions", "summary", "retention_analysis", - "detailed_description", "soundscape", - )) or bool(ref.get("subject_defs")) or bool(ref.get("retention")) + return isinstance(ref, dict) and ( + any(_ensure_str(ref.get(key)) for key in ( + "subject_definitions", "summary", "retention_analysis", + "detailed_description", "soundscape", + )) or bool(ref.get("subject_defs")) or bool(ref.get("retention")) + ) def migrate_legacy_prompt(builder: dict, timeline_state: dict, widget_prompt: object = "") -> bool: - """Preserve old Director prompts as a lossless Simple prompt. - - Earlier Director revisions stored the editable prompt in the normal prompt - widget or timeline JSON, before ``builder_state`` existed. Prefer the - previously resolved text because it is the exact generation prompt; fall - back to the old global prompt plus ordered prompt blocks. Existing builder - content always wins, so loading a current workflow is idempotent. - """ + """Move a pre-builder Director prompt into lossless Simple-prompt state.""" if not isinstance(builder, dict) or has_builder_content(builder): return False state = timeline_state if isinstance(timeline_state, dict) else {} - candidates = [state.get("resolved_prompt"), state.get("full_prompt")] - legacy_global = widget_prompt if _ensure_str(widget_prompt) else state.get("prompt") raw_blocks = state.get("prompt_blocks") blocks = raw_blocks if isinstance(raw_blocks, list) else [] ordered_blocks = sorted(enumerate(blocks), key=lambda pair: ( @@ -291,11 +278,15 @@ def migrate_legacy_prompt(builder: dict, timeline_state: dict, widget_prompt: ob int(pair[1].get("order", pair[0])) if isinstance(pair[1], dict) else pair[0], pair[0], )) - candidates.append("\n".join([ - _ensure_str(legacy_global), - *[_ensure_str(block.get("text")) for _, block in ordered_blocks - if isinstance(block, dict) and block.get("enabled", True)], - ])) + legacy_global = widget_prompt if _ensure_str(widget_prompt) else state.get("prompt") + candidates = [ + state.get("resolved_prompt"), + state.get("full_prompt"), + "\n".join([_ensure_str(legacy_global), *[ + _ensure_str(block.get("text")) for _, block in ordered_blocks + if isinstance(block, dict) and block.get("enabled", True) + ]]), + ] for candidate in candidates: text = _ensure_str(candidate) if text: diff --git a/nodes/nodes_minimax_h3_director.py b/nodes/nodes_minimax_h3_director.py index 9a523fa..2d7f8bc 100644 --- a/nodes/nodes_minimax_h3_director.py +++ b/nodes/nodes_minimax_h3_director.py @@ -183,8 +183,10 @@ class MiniMaxH3Director: resolved = external_prompt_overwrite else: resolved = build_prompt(merged) - if not any(str(merged.get(key) or "").strip() for key in ("imd", "soundscape")) and mode != "REF2VA": - resolved = build_prompt(merged) if migrated_legacy_prompt else assemble_prompt(prompt, blocks) + if (not migrated_legacy_prompt and + not any(str(merged.get(key) or "").strip() for key in ("imd", "soundscape")) and + mode != "REF2VA"): + resolved = assemble_prompt(prompt, blocks) for issue in validate_builder_state(merged): log_dasiwa("MiniMax H3 Director", f"[{issue['level'].upper()}] {issue['msg']}") guide = { diff --git a/test/test_minimax_h3_director_prompt_compatibility.py b/test/test_minimax_h3_director_prompt_compatibility.py index 89343c1..62a92da 100644 --- a/test/test_minimax_h3_director_prompt_compatibility.py +++ b/test/test_minimax_h3_director_prompt_compatibility.py @@ -1,4 +1,4 @@ -"""Regression tests for preserving pre-builder MiniMax Director prompts.""" +"""Regression tests for Director prompt migration and standard prompt output.""" from nodes.helper_minimax_h3_prompt_builder import ( build_prompt, default_builder_state, @@ -7,56 +7,28 @@ from nodes.helper_minimax_h3_prompt_builder import ( from nodes.nodes_minimax_h3_director import MiniMaxH3Director -def test_migrates_legacy_widget_prompt_losslessly(): +def test_legacy_widget_prompt_is_preserved_losslessly(): + prompt = "Detailed old prompt\n[English] Exact dialogue." builder = default_builder_state("FL2VA") - prompt = "A complex scene\n[English] Keep every token." assert migrate_legacy_prompt(builder, {}, prompt) assert builder["prompt_mode"] == "simple" - assert builder["simple_prompt"] == prompt assert build_prompt(builder) == prompt -def test_prefers_exact_prior_resolved_prompt_over_legacy_parts(): - builder = default_builder_state("REF2VA") - state = { - "resolved_prompt": "Exact former generation prompt\nwith all sections intact", - "prompt": "short old prompt", - "prompt_blocks": [{"text": "old block"}], - } - - assert migrate_legacy_prompt(builder, state, "widget prompt") - assert build_prompt(builder) == state["resolved_prompt"] - - -def test_legacy_prompt_blocks_are_ordered_and_disabled_blocks_excluded(): - builder = default_builder_state("T2VA") - state = { - "prompt": "Global prompt", - "prompt_blocks": [ - {"text": "Second", "start": 2, "order": 1}, - {"text": "Disabled", "start": 0, "enabled": False}, - {"text": "First", "start": 1, "order": 0}, - ], - } - - assert migrate_legacy_prompt(builder, state) - assert build_prompt(builder) == "Global prompt\nFirst\nSecond" - - -def test_never_overwrites_current_builder_content(): - builder = default_builder_state("FL2VA") - builder["imd"] = "Current authored prompt" - - assert not migrate_legacy_prompt(builder, {"resolved_prompt": "Old prompt"}, "Old widget") - assert "simple_prompt" not in builder - - -def test_director_execution_preserves_legacy_prompt_in_guide(): - prompt = "Detailed legacy prompt\n[English] Exact dialogue." +def test_director_uses_legacy_prompt_when_builder_is_absent(): + prompt = "Former video generation prompt" guide = MiniMaxH3Director().build_guide( "T2VA", prompt, 1344, 768, 5, "match", "{}", "", )[0] assert guide["resolved_prompt"] == prompt - assert guide["builder_state"]["simple_prompt"] == prompt \ No newline at end of file + assert guide["builder_state"]["simple_prompt"] == prompt + + +def test_frontend_keeps_standard_prompt_widget_serialized(): + source = open("js/minimax_h3_director.js", encoding="utf-8").read() + + assert "promptWidget.value = resolved" in source + assert "promptWidget.callback?.(resolved)" in source + assert "state.resolved_prompt = resolved" in source