fix: preserve legacy Director prompts
This commit is contained in:
@@ -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 => {
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user