fix: preserve legacy Director prompts

This commit is contained in:
Darksidewalker
2026-08-16 21:45:08 +02:00
parent da331c78b4
commit 36b9b3a788
5 changed files with 151 additions and 3 deletions
+28
View File
@@ -51,6 +51,23 @@ function installStyles() {
}
function parseState(value) { try { const s = JSON.parse(value || "{}"); return { ...DEFAULT_STATE, ...s, items: Array.isArray(s.items) ? s.items : [], prompt_blocks: Array.isArray(s.prompt_blocks) ? s.prompt_blocks : [] }; } catch { return structuredClone(DEFAULT_STATE); } }
function textValue(value) { return typeof value === "string" ? value.trim() : ""; }
function builderHasContent(builder) {
if (!builder || typeof builder !== "object") return false;
if (textValue(builder.simple_prompt) || textValue(builder.imd) || textValue(builder.soundscape)) return true;
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)
));
}
function legacyPrompt(state, widgetPrompt) {
for (const candidate of [state?.resolved_prompt, state?.full_prompt]) if (textValue(candidate)) return textValue(candidate);
const global = textValue(widgetPrompt) || textValue(state?.prompt);
const blocks = Array.isArray(state?.prompt_blocks) ? [...state.prompt_blocks].sort((a, b) => (Number(a?.start) || 0) - (Number(b?.start) || 0) || (Number(a?.order) || 0) - (Number(b?.order) || 0)) : [];
return [global, ...blocks.filter(block => block?.enabled !== false).map(block => textValue(block?.text)).filter(Boolean)].filter(Boolean).join("\n");
}
function viewUrl(path) { return api.apiURL(`/view?filename=${encodeURIComponent(path)}&type=input`); }
function count(state, type) { return state.items.filter(i => i.enabled !== false && i.type === type).length; }
function idFor(type, n) { return `${type}-${Date.now()}-${n}`; }
@@ -834,6 +851,17 @@ 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();
}
}
syncNodeBounds();
render();
void Promise.all(state.items.filter(item => item.type === "video" && !item.thumbnail).map(async item => {
+56
View File
@@ -249,6 +249,62 @@ def _build_simple_prompt(state: dict) -> str:
return "\n".join(lines)
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.
"""
if not isinstance(state, dict):
return False
if _ensure_str(state.get("simple_prompt")):
return True
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"))
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.
"""
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: (
float(pair[1].get("start", 0)) if isinstance(pair[1], dict) else 0,
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)],
]))
for candidate in candidates:
text = _ensure_str(candidate)
if text:
builder["prompt_mode"] = "simple"
builder["simple_prompt"] = text
return True
return False
def build_prompt(state: dict) -> str:
"""Mode-dispatched prompt assembly honoring the requested style.
+4 -2
View File
@@ -8,7 +8,8 @@ from .helper_minimax_h3_director import (
scale_input_media, validate_reference_limits,
)
from .helper_minimax_h3_prompt_builder import (
build_prompt, default_builder_state, normalize_ref_schema, validate_builder_state,
build_prompt, default_builder_state, migrate_legacy_prompt, normalize_ref_schema,
validate_builder_state,
)
BASE_MODES = {"T2VA", "I2VA", "FL2VA", "L2VA"}
@@ -99,6 +100,7 @@ class MiniMaxH3Director:
normalize_ref_schema(merged["ref"])
merged["mode"] = mode
merged["duration"] = duration
migrated_legacy_prompt = migrate_legacy_prompt(merged, state, prompt)
items = sorted(enumerate(state.get("items", [])), key=lambda pair: (int(pair[1].get("order", pair[0])), pair[0]))
items = [pair for pair in items if pair[1].get("enabled", True)]
@@ -182,7 +184,7 @@ class MiniMaxH3Director:
else:
resolved = build_prompt(merged)
if not any(str(merged.get(key) or "").strip() for key in ("imd", "soundscape")) and mode != "REF2VA":
resolved = assemble_prompt(prompt, blocks)
resolved = build_prompt(merged) if migrated_legacy_prompt else assemble_prompt(prompt, blocks)
for issue in validate_builder_state(merged):
log_dasiwa("MiniMax H3 Director", f"[{issue['level'].upper()}] {issue['msg']}")
guide = {
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "ComfyUI-DaSiWa-Nodes"
description = "DaSiWa Custom Nodes Collection"
version = "0.4.15"
version = "0.4.16"
license = {file = "LICENSE"}
dependencies = [
"torch",
@@ -0,0 +1,62 @@
"""Regression tests for preserving pre-builder MiniMax Director prompts."""
from nodes.helper_minimax_h3_prompt_builder import (
build_prompt,
default_builder_state,
migrate_legacy_prompt,
)
from nodes.nodes_minimax_h3_director import MiniMaxH3Director
def test_migrates_legacy_widget_prompt_losslessly():
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>"
guide = MiniMaxH3Director().build_guide(
"T2VA", prompt, 1344, 768, 5, "match", "{}", "",
)[0]
assert guide["resolved_prompt"] == prompt
assert guide["builder_state"]["simple_prompt"] == prompt