fix: serialize Director prompts
This commit is contained in:
@@ -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, <Picture 1> (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 — <Picture 1> (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 => {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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<d>[English] Exact dialogue.</d>"
|
||||
builder = default_builder_state("FL2VA")
|
||||
prompt = "A complex scene\n<d>[English] Keep every token.</d>"
|
||||
|
||||
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<d>[English] Exact dialogue.</d>"
|
||||
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
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user