Fix Shotboard V3 persistence bugs for 1.4.6

This commit is contained in:
IAMCCS
2026-05-22 20:17:36 +02:00
parent 5445dc1489
commit 949236ec8a
4 changed files with 548 additions and 66 deletions
+95 -12
View File
@@ -2706,6 +2706,22 @@ class IAMCCS_CineShotboardPlannerV3(IAMCCS_CineShotboardPlannerPro):
f"rebuilt_legacy_prompts={rebuilt_step_prompts} "
f"mode={flfreal_mode}"
)
if promptrelay_requested and local_parts:
local_hash = hashlib.sha1(str(resources.get("cine_local_prompts", "") or "").encode("utf-8", errors="ignore")).hexdigest()[:12]
print(
"[IAMCCS FLFReal] "
f"LOCAL_PROMPTS_USED source=shotboard_v3_timeline "
f"count={len(local_parts)} segments={len(length_parts)} "
f"local_hash={local_hash}"
)
for idx, prompt in enumerate(local_parts[:50]):
compact = str(prompt or "").replace("\n", " ")
if len(compact) > 360:
compact = compact[:357] + "..."
length = length_parts[idx] if idx < len(length_parts) else "<missing>"
print(f"[IAMCCS FLFReal] local[{idx:02d}] length={length} prompt={compact!r}")
if len(local_parts) > 50:
print(f"[IAMCCS FLFReal] local prompt log truncated: {len(local_parts) - 50} more prompts.")
cine_linx["mode"] = "cine_ltx23_filmmaker_timeline"
cine_linx["chain"][0]["name"] = "Cine Shotboard Planner V3"
cine_linx["stages"][0]["kind"] = "cine_filmmaker_timeline_planner"
@@ -3088,6 +3104,13 @@ class IAMCCS_CineInfo:
length_parts = [part for part in re.split(r"[,;\s]+", str(segment_lengths or "")) if part.strip()]
global_hash = hashlib.sha1(str(global_prompt or "").encode("utf-8", errors="ignore")).hexdigest()[:12]
local_hash = hashlib.sha1(str(local_prompts or "").encode("utf-8", errors="ignore")).hexdigest()[:12]
relay_prompt_log = []
for idx, prompt in enumerate(locals_list):
relay_prompt_log.append({
"index": idx,
"segment_length": length_parts[idx] if idx < len(length_parts) else "<missing>",
"prompt": prompt,
})
flf_hash = hashlib.sha1(str(flf_timeline or "").encode("utf-8", errors="ignore")).hexdigest()[:12]
image_shape = [int(v) for v in multi_output.shape] if torch.is_tensor(multi_output) else []
_cine_debug(
@@ -4005,6 +4028,14 @@ class IAMCCS_CineFilmmakerBackend:
local_parts = [part.strip() for part in str(local_prompts or "").split("|") if part.strip()]
length_parts = [part for part in re.split(r"[,;\s]+", str(segment_lengths or "")) if part.strip()]
local_hash = hashlib.sha1(str(local_prompts or "").encode("utf-8", errors="ignore")).hexdigest()[:12]
relay_prompt_log = [
{
"index": idx,
"segment_length": length_parts[idx] if idx < len(length_parts) else "<missing>",
"prompt": prompt,
}
for idx, prompt in enumerate(local_parts)
]
print(
"[IAMCCS FilmmakerBackend] "
f"PromptRelay requested={bool(promptrelay_requested)} "
@@ -4021,6 +4052,25 @@ class IAMCCS_CineFilmmakerBackend:
if safety_budget_enabled and safety_budget > 0 and prompt_budget > safety_budget:
raise RuntimeError(f"PromptRelay disabled by optional safety budget: estimated matrix load {prompt_budget} > {safety_budget}")
promptrelay_nodes = _load_original_promptrelay_module()
print(
"[IAMCCS FilmmakerBackend] "
f"PROMPT_RELAY_LOCAL_PROMPTS_USED source=cine_linx "
f"count={len(local_parts)} segments={len(length_parts)} "
f"global_hash={hashlib.sha1(str(global_prompt or '').encode('utf-8', errors='ignore')).hexdigest()[:12]} "
f"local_hash={local_hash}"
)
for item in relay_prompt_log[:50]:
compact = str(item["prompt"]).replace("\n", " ")
if len(compact) > 360:
compact = compact[:357] + "..."
print(
"[IAMCCS FilmmakerBackend] "
f"relay[{int(item['index']):02d}] "
f"length={item['segment_length']} "
f"prompt={compact!r}"
)
if len(relay_prompt_log) > 50:
print(f"[IAMCCS FilmmakerBackend] relay prompt log truncated: {len(relay_prompt_log) - 50} more prompts.")
patched_model, positive = promptrelay_nodes._encode_relay(
model,
clip,
@@ -4087,6 +4137,8 @@ class IAMCCS_CineFilmmakerBackend:
"promptrelay_error": relay_error,
"promptrelay_safety_budget_enabled": bool(safety_budget_enabled),
"promptrelay_safety_budget": int(safety_budget),
"local_hash": local_hash,
"local_prompts_used": relay_prompt_log,
"duration_seconds": float(duration_seconds),
"frame_rate": int(frame_rate),
"max_frames": int(max_frames),
@@ -4952,28 +5004,40 @@ class IAMCCS_CineRelayOrBypass:
outputs = cls._outputs(cine_linx)
payload = cls._payload(cine_linx, resources)
source = "explicit_inputs" if local_prompts is not None else "cine_linx"
resolved_local_prompts = str(local_prompts if local_prompts is not None else (
cine_local_prompts = str(
resources.get("cine_local_prompts",
outputs.get("local_prompts",
payload.get("local_prompts", "")))
) or "")
resolved_segment_lengths = str(segment_lengths if segment_lengths is not None else (
or ""
)
cine_segment_lengths = str(
resources.get("cine_segment_lengths",
outputs.get("segment_lengths",
payload.get("segment_lengths", "")))
) or "")
resolved_global_prompt = str(global_prompt if global_prompt is not None else (
or ""
)
cine_global_prompt = str(
resources.get("cine_global_prompt",
outputs.get("global_prompt",
payload.get("global_prompt", "")))
) or "")
resolved_epsilon = _safe_float(
epsilon if epsilon is not None else resources.get("cine_promptrelay_epsilon",
outputs.get("promptrelay_epsilon",
payload.get("promptrelay_epsilon", 1e-3))),
1e-3,
or ""
)
cine_epsilon = resources.get("cine_promptrelay_epsilon",
outputs.get("promptrelay_epsilon",
payload.get("promptrelay_epsilon", None)))
if cine_local_prompts.strip():
source = "cine_linx"
resolved_local_prompts = cine_local_prompts
resolved_segment_lengths = cine_segment_lengths
resolved_global_prompt = cine_global_prompt or str(global_prompt or "")
resolved_epsilon = _safe_float(cine_epsilon if cine_epsilon is not None else epsilon, 1e-3)
else:
source = "explicit_inputs" if local_prompts is not None else "cine_linx"
resolved_local_prompts = str(local_prompts if local_prompts is not None else cine_local_prompts or "")
resolved_segment_lengths = str(segment_lengths if segment_lengths is not None else cine_segment_lengths or "")
resolved_global_prompt = str(global_prompt if global_prompt is not None else cine_global_prompt or "")
resolved_epsilon = _safe_float(epsilon if epsilon is not None else cine_epsilon, 1e-3)
active = bool(resolved_local_prompts.strip())
return active, resolved_local_prompts, resolved_segment_lengths, resolved_global_prompt, resolved_epsilon, source
@@ -5117,11 +5181,30 @@ class IAMCCS_CineRelayOrBypass:
"prompt_source": prompt_source,
"first_local": locals_list[0][:220] if locals_list else "",
"last_local": locals_list[-1][:220] if locals_list else "",
"local_prompts_used": relay_prompt_log,
"truth": (
"Relay active. Called ComfyUI-PromptRelay _encode_relay 1:1. "
"By Carmine Cristallo Scalzi AI research (IAMCCS) - patreon.com/IAMCCS"
),
})
print(
"[IAMCCS CineRelayOrBypass] "
f"PROMPT_RELAY_LOCAL_PROMPTS_USED source={prompt_source} "
f"count={len(locals_list)} segments={len(length_parts)} "
f"global_hash={global_hash} local_hash={local_hash}"
)
for item in relay_prompt_log[:50]:
compact = str(item["prompt"]).replace("\n", " ")
if len(compact) > 360:
compact = compact[:357] + "..."
print(
"[IAMCCS CineRelayOrBypass] "
f"relay[{int(item['index']):02d}] "
f"length={item['segment_length']} "
f"prompt={compact!r}"
)
if len(relay_prompt_log) > 50:
print(f"[IAMCCS CineRelayOrBypass] relay prompt log truncated: {len(relay_prompt_log) - 50} more prompts.")
_cine_debug(
"[IAMCCS CineRelayOrBypass] "
f"mode=PROMPT_RELAY_ORIGINAL_1TO1 promptrelay_enabled=True "
+12 -5
View File
@@ -46,7 +46,7 @@ _PRESETS: dict[str, dict[str, DetailAtelierPreset]] = {
},
"12GB": {
"preview": DetailAtelierPreset(960, 40, 16, 0.45, 0.90, 1.0, 1, 1, 1, 384, 48, 128, 16, 2.0, True, False, False, "off", "Preview for 12GB cards."),
"balanced": DetailAtelierPreset(1280, 48, 16, 0.50, 1.00, 1.0, 1, 1, 1, 384, 48, 128, 16, 2.0, True, True, False, "rtx_x2", "Recommended 12GB daily preset."),
"balanced": DetailAtelierPreset(1280, 56, 24, 0.50, 1.00, 1.0, 1, 1, 1, 384, 48, 128, 16, 2.0, True, True, False, "rtx_x2", "12GB fidelity preset aligned with the reference LTX looper."),
"quality": DetailAtelierPreset(1536, 56, 24, 0.55, 1.00, 1.0, 1, 1, 1, 384, 48, 128, 16, 2.5, True, True, True, "rtx_x2", "Final-ish 12GB preset; interpolation is optional."),
},
"16GB": {
@@ -214,7 +214,7 @@ def _auto_adjust_loop_values(
max_tile_by_vram = {
"8GB": 40,
"12GB": 48,
"12GB": 56,
"16GB": 64,
"24GB": 80,
}
@@ -226,7 +226,7 @@ def _auto_adjust_loop_values(
}
if low_ram:
max_tile_by_vram["8GB"] = 32
max_tile_by_vram["12GB"] = 48
max_tile_by_vram["12GB"] = 56
max_tile = max_tile_by_vram.get(effective_vram, 48)
min_tile = min_tile_by_vram.get(effective_vram, 40)
@@ -237,7 +237,7 @@ def _auto_adjust_loop_values(
decisions.append(f"temporal_tile_size:floor {temporal_tile_size}->{min_tile}")
temporal_tile_size = min_tile
max_overlap = 16 if effective_vram in {"8GB", "12GB"} else 24
max_overlap = 16 if effective_vram == "8GB" else 24
if int(temporal_overlap) > max_overlap:
decisions.append(f"temporal_overlap:cap {temporal_overlap}->{max_overlap}")
temporal_overlap = max_overlap
@@ -601,6 +601,11 @@ class IAMCCS_DetailAtelierSampler:
preset.temporal_overlap_cond_strength,
)
cond_image_strength = adv_float("cond_image_strength", preset.cond_image_strength)
has_manual_loop_override = False
try:
has_manual_loop_override = int(advanced.get("temporal_tile_size", 0)) > 0 or int(advanced.get("temporal_overlap", 0)) > 0
except Exception:
has_manual_loop_override = False
horizontal_tiles = adv_int("horizontal_tiles", preset.horizontal_tiles)
vertical_tiles = adv_int("vertical_tiles", preset.vertical_tiles)
@@ -628,7 +633,7 @@ class IAMCCS_DetailAtelierSampler:
int(latent_info.get("time_scale_factor") or 8),
)
}
if str(hardware_mode or "auto") == "auto":
if str(hardware_mode or "auto") == "auto" and not has_manual_loop_override:
vram, auto_values, auto_decisions = _auto_adjust_loop_values(
selected_vram=requested_vram,
quality=quality,
@@ -646,6 +651,8 @@ class IAMCCS_DetailAtelierSampler:
horizontal_tiles = int(auto_values["horizontal_tiles"])
vertical_tiles = int(auto_values["vertical_tiles"])
spatial_overlap = int(auto_values["spatial_overlap"])
elif str(hardware_mode or "auto") == "auto" and has_manual_loop_override:
auto_decisions.append("advanced_loop_override:respect_temporal_values")
adain = adv_float("adain_factor", float(adain_factor))
guide_start = adv_int("guiding_start_step", int(guiding_start_step))
+438 -46
View File
@@ -1,9 +1,9 @@
import { app } from "../../scripts/app.js";
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
const CINE_VERSION = "2026-05-19-shotboard-package-self-contained-import-1";
const CINE_VERSION = "2026-05-22-v3-node-all-values-truth-1";
const SHOTBOARD_V3_RIGID_WIDTH = 1920;
const SHOTBOARD_V3_OPEN_HEIGHT = 760;
const SHOTBOARD_V3_OPEN_HEIGHT = 900;
const SHOTBOARD_V3_COLLAPSED_HEIGHT = 560;
const SHOTBOARD_NODE_MIN_SIZE = [1500, 760];
const SHOTBOARD_NODE_DEFAULT_SIZE = [1500, 780];
@@ -209,12 +209,34 @@ function setWidgetValue(node, name, value) {
const widget = getWidget(node, name);
if (!widget) return false;
widget.value = value;
syncWidgetSerializedValue(node, widget, value);
try { widget.callback?.(value, app.canvas, node); } catch {}
try { node.setDirtyCanvas?.(true, true); app.graph?.setDirtyCanvas?.(true, true); } catch {}
try { node.graph?.change?.(); app.graph?.change?.(); } catch {}
return true;
}
function syncWidgetSerializedValue(node, widget, value) {
if (!node || !widget) return false;
const index = Array.isArray(node.widgets) ? node.widgets.indexOf(widget) : -1;
if (index < 0) return false;
if (!Array.isArray(node.widgets_values)) node.widgets_values = [];
node.widgets_values[index] = value;
return true;
}
function syncSerializedWidgetValue(node, serialized, name, value) {
const widget = getWidget(node, name);
if (!widget || !serialized) return false;
const index = Array.isArray(node.widgets) ? node.widgets.indexOf(widget) : -1;
if (index < 0) return false;
if (!Array.isArray(serialized.widgets_values)) serialized.widgets_values = [];
serialized.widgets_values[index] = value;
widget.value = value;
syncWidgetSerializedValue(node, widget, value);
return true;
}
function lockNodeMinimumSize(node, minSize, options = {}) {
if (!node || !Array.isArray(minSize)) return;
const minWidth = Number(minSize[0]) || 0;
@@ -482,7 +504,7 @@ function protectControlDrag(element) {
element.setAttribute?.("draggable", "false");
for (const eventName of ["pointerdown", "pointermove", "mousedown", "mousemove", "touchstart", "touchmove", "wheel", "drag", "dragover", "drop"]) {
element.addEventListener(eventName, (event) => {
if ((eventName === "dragover" || eventName === "drop") && hasJsonBoardDrag(event)) return;
if ((eventName === "dragover" || eventName === "drop") && hasFileDrag(event)) return;
if ((eventName === "pointerdown" || eventName === "pointermove" || eventName === "mousedown" || eventName === "mousemove") && isNumericStepDragTarget(event)) return;
event.stopPropagation();
}, { passive: false, capture: true });
@@ -510,6 +532,14 @@ function hasJsonBoardDrag(event) {
});
}
function hasFileDrag(event) {
const transfer = event?.dataTransfer;
if (!transfer) return false;
if (Array.from(transfer.files || []).length) return true;
if (Array.from(transfer.items || []).some((item) => item?.kind === "file")) return true;
return Array.from(transfer.types || []).includes("Files");
}
function protectDragHandle(element) {
if (!element) return element;
for (const eventName of ["pointerdown", "mousedown", "touchstart"]) {
@@ -1832,6 +1862,22 @@ function replaceReferencePathAt(node, index, newPath) {
return compact;
}
function syncSegmentImageFileForReference(segments, refIndex, imagePath) {
const refNumber = Math.max(1, Math.round(Number(refIndex || 0) + 1));
const cleanPath = String(imagePath || "").trim();
if (!Array.isArray(segments) || !cleanPath) return false;
let changed = false;
for (const seg of segments) {
if (!seg || typeof seg !== "object") continue;
if (String(seg.type || "image") === "text" || String(seg.type || "image") === "audio") continue;
if (Math.round(Number(seg.ref || 0)) !== refNumber) continue;
seg.imageFile = cleanPath;
delete seg.image_file;
changed = true;
}
return changed;
}
function isShotboardV2Node(node) {
return nodeClassName(node) === "IAMCCS_CineShotboardPlannerProV2";
}
@@ -2836,8 +2882,8 @@ function timeControl(value, onChange, frameRate = null, opts = {}) {
const dur = Math.max(0, end - start);
const durText = formatTimeValue(dur);
const frameText = validFps ? ` (${Math.round(dur * validFps)}f)` : "";
durationLine.textContent = `⏱ ${durText}s${frameText} - segment duration`;
rangeLine.textContent = `${formatTimeValue(start)}s → ${formatTimeValue(end)}s`;
durationLine.textContent = `⏱ ${durText}s${frameText} - segment duration`;
rangeLine.textContent = `${formatTimeValue(start)}s → ${formatTimeValue(end)}s`;
}
updateInfo();
@@ -5827,6 +5873,41 @@ function renderShotboardV3(node) {
const defaultForceWidget = getWidget(node, "default_force");
const imageWidthWidget = getWidget(node, "image_width");
const imageHeightWidget = getWidget(node, "image_height");
const v3SettingNames = [
"duration_seconds",
"frame_rate",
"default_force",
"promptrelay_epsilon",
"image_width",
"image_height",
"image_resize_method",
"image_multiple_of",
"img_compression",
"guide_policy",
"min_guide_gap_seconds",
"max_guides",
];
const clearV3BoardTransientState = () => {
node.properties = node.properties || {};
[
"iamccs_v3_imported_board",
"iamccs_v3_imported_board_backup",
"iamccs_v3_imported_timeline_data",
"iamccs_v3_imported_settings",
"iamccs_v3_board",
"iamccs_v3_board_settings",
"iamccs_v3_board_data",
].forEach((key) => {
if (Object.prototype.hasOwnProperty.call(node.properties, key)) delete node.properties[key];
});
};
const v3SettingsSnapshot = () => Object.fromEntries(v3SettingNames
.filter((name) => getWidget(node, name))
.map((name) => {
const value = getWidget(node, name)?.value;
return [name, name === "image_resize_method" ? cineResizeMethodValue(value) : value];
}));
clearV3BoardTransientState();
if (defaultForceWidget) defaultForceWidget.value = Math.max(0, Math.min(1, Number(defaultForceWidget.value || 0)));
let collapsed = Boolean(node.properties?.iamccs_v3_collapsed);
let promptTextScale = Math.max(0.85, Math.min(1.55, Number(node.properties?.iamccs_v3_prompt_text_scale || 1)));
@@ -5855,9 +5936,75 @@ function renderShotboardV3(node) {
const defaultLen = () => Math.max(1, Math.round(getFps() * 3));
const newId = (prefix) => `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
const timelinePromptStats = (data) => {
const prompts = [];
if (data && typeof data === "object") {
if (Array.isArray(data.segments)) {
data.segments.forEach((seg) => {
const prompt = String(seg?.prompt || "").trim();
if (prompt) prompts.push(prompt);
});
}
if (Array.isArray(data.rows)) {
data.rows.forEach((row) => {
const prompt = String(row?.relay_prompt ?? row?.local_prompt ?? row?.prompt ?? "").trim();
if (prompt) prompts.push(prompt);
});
}
for (const key of ["director_local_prompts", "local_prompts"]) {
const value = String(data[key] || "").trim();
if (value) value.split("|").forEach((part) => {
const prompt = String(part || "").trim();
if (prompt) prompts.push(prompt);
});
}
}
const joined = prompts.join(" | ");
return {
promptCount: prompts.length,
containsCoastline: /\bcoastline\b/i.test(joined),
firstPrompt: prompts[0] || "",
};
};
function readTimeline() {
try {
const data = JSON.parse(String(timelineWidget?.value || "{}"));
const candidates = [];
const widgetText = String(timelineWidget?.value || "").trim();
const backupText = String(node.properties?.iamccs_v3_timeline_data_backup || "").trim();
let widgetCandidate = null;
let backupCandidate = null;
if (widgetText) {
try {
const value = JSON.parse(widgetText);
widgetCandidate = { source: "widget.timeline_data", textLength: widgetText.length, value, stats: timelinePromptStats(value) };
candidates.push(widgetCandidate);
} catch {}
}
if (backupText) {
try {
const value = JSON.parse(backupText);
backupCandidate = { source: "properties.iamccs_v3_timeline_data_backup", textLength: backupText.length, value, stats: timelinePromptStats(value) };
candidates.push(backupCandidate);
} catch {}
}
const selected = widgetCandidate || backupCandidate || { source: "empty", value: {} };
const data = selected.value;
console.log("[IAMCCS V3 TIMELINE LOAD]", {
selected: selected.source,
truth: widgetCandidate ? "node.timeline_data" : "backup_fallback",
nodeId: node?.id,
candidates: candidates.map((candidate) => ({
source: candidate.source,
textLength: candidate.textLength,
hasSegments: Array.isArray(candidate.value?.segments),
segmentCount: Array.isArray(candidate.value?.segments) ? candidate.value.segments.length : 0,
rowCount: Array.isArray(candidate.value?.rows) ? candidate.value.rows.length : 0,
promptCount: candidate.stats?.promptCount || 0,
containsCoastline: Boolean(candidate.stats?.containsCoastline),
firstPrompt: String(candidate.stats?.firstPrompt || "").slice(0, 220),
})),
});
if (data && typeof data === "object") {
return {
schema: data.schema || "iamccs.cine.filmmaker_timeline",
@@ -6035,12 +6182,29 @@ function renderShotboardV3(node) {
};
};
let pendingTimelineCommit = 0;
let promptEditCounter = 0;
const markPromptFieldEdited = (field) => {
if (!field?.dataset) return "";
const stamp = `${Date.now()}-${++promptEditCounter}`;
field.dataset.iamccsV3EditedAt = stamp;
return stamp;
};
const fieldEditStamp = (field) => {
const raw = String(field?.dataset?.iamccsV3EditedAt || "");
const [time, order] = raw.split("-");
return (Number(time) || 0) * 100000 + (Number(order) || 0);
};
const isTextEditing = () => {
const active = document.activeElement;
return Boolean(active && root.contains(active) && /^(TEXTAREA|INPUT)$/i.test(active.tagName || ""));
};
const commitTimelineJson = (json, force = false) => {
if (timelineWidget) timelineWidget.value = json;
if (timelineWidget) {
timelineWidget.value = json;
syncWidgetSerializedValue(node, timelineWidget, json);
}
node.properties = node.properties || {};
node.properties.iamccs_v3_timeline_data_backup = json;
if (pendingTimelineCommit) {
clearTimeout(pendingTimelineCommit);
pendingTimelineCommit = 0;
@@ -6054,17 +6218,51 @@ function renderShotboardV3(node) {
setWidgetValue(node, "timeline_data", timelineWidget?.value ?? json);
}, 220);
};
const syncTimelineTextFromDom = () => {
let changed = 0;
const active = document.activeElement;
const fields = Array.from(root.querySelectorAll("[data-iamccs-v3-segment-id][data-iamccs-v3-key]"))
.sort((a, b) => {
if (a === active) return -1;
if (b === active) return 1;
return fieldEditStamp(b) - fieldEditStamp(a);
});
fields.forEach((el) => {
const key = String(el.dataset.iamccsV3Key || "");
if (key !== "prompt") return;
const segmentId = String(el.dataset.iamccsV3SegmentId || "");
if (!segmentId) return;
const seg = (timeline.segments || []).find((item) => String(item?.id || "") === segmentId);
if (!seg) return;
const value = String(el.value ?? "");
if (String(seg.prompt ?? "") === value) return;
seg.prompt = value;
seg.use_prompt = Boolean(value.trim());
changed += 1;
syncSegmentTextPeers(segmentId, "prompt", value, el);
syncSegmentRelayPeers(segmentId, Boolean(seg.use_prompt), null);
});
if (changed) {
console.log("[IAMCCS V3 PERSIST] DOM text authority sync", { changed });
}
return changed;
};
const flushTimelineWrite = () => {
syncTimelineTextFromDom();
if (pendingTimelineCommit) {
clearTimeout(pendingTimelineCommit);
pendingTimelineCommit = 0;
setWidgetValue(node, "timeline_data", timelineWidget?.value || "");
const value = timelineWidget?.value || "";
if (timelineWidget) syncWidgetSerializedValue(node, timelineWidget, value);
setWidgetValue(node, "timeline_data", value);
}
};
const syncSegmentTextPeers = (segmentId, key, value, source) => {
const sourceStamp = String(source?.dataset?.iamccsV3EditedAt || "");
root.querySelectorAll(`[data-iamccs-v3-segment-id="${String(segmentId)}"][data-iamccs-v3-key="${String(key)}"]`).forEach((el) => {
if (el === source) return;
if (el.value !== value) el.value = value;
if (sourceStamp && el.dataset) el.dataset.iamccsV3EditedAt = sourceStamp;
});
};
const syncSegmentRelayPeers = (segmentId, checked, source) => {
@@ -6073,7 +6271,24 @@ function renderShotboardV3(node) {
if (el.checked !== checked) el.checked = checked;
});
};
const logPromptPersistence = (seg, source) => {
try {
const prompt = String(seg?.prompt || "");
const preview = prompt.replace(/\s+/g, " ").slice(0, 220);
console.log("[IAMCCS V3 PERSIST]", {
source,
segmentId: seg?.id,
label: seg?.label,
promptLength: prompt.length,
promptPreview: preview,
timelineWidgetLength: String(timelineWidget?.value || "").length,
widgetIndex: Array.isArray(node.widgets) ? node.widgets.indexOf(timelineWidget) : -1,
widgetsValueLength: Array.isArray(node.widgets_values) && timelineWidget ? String(node.widgets_values[node.widgets.indexOf(timelineWidget)] || "").length : 0,
});
} catch {}
};
const writeTimeline = (options = {}) => {
if (!options.skipDomSync) syncTimelineTextFromDom();
neutralizeLegacyStepTransitions();
enforceDurationMinimum();
cleanupAudioPlaceholdersOverlappingMedia();
@@ -6314,7 +6529,13 @@ function renderShotboardV3(node) {
promptArea.value = String(promptWidget?.value || "");
promptArea.rows = 3;
promptArea.style.cssText = inputBase() + `background:${purple.valueBg};border-color:${purple.border};color:${purple.valueText};resize:vertical;min-height:64px;font-weight:700;font-size:${promptFontSize(12)};`;
promptArea.oninput = () => setWidgetValue(node, "global_prompt", promptArea.value);
promptArea.oninput = () => {
setWidgetValue(node, "global_prompt", promptArea.value);
node.properties = node.properties || {};
node.properties.iamccs_v3_global_prompt_backup = promptArea.value;
};
promptArea.onchange = promptArea.oninput;
promptArea.onblur = promptArea.oninput;
promptWrap.append(promptLabel, promptArea);
const settings = document.createElement("div");
@@ -7607,15 +7828,40 @@ function renderShotboardV3(node) {
caption.onclick = (event) => event.stopPropagation();
caption.ondblclick = (event) => event.stopPropagation();
caption.oninput = () => {
markPromptFieldEdited(caption);
seg.prompt = caption.value;
seg.use_prompt = Boolean(String(caption.value || "").trim());
if (isActionBridgeRelaySegment(seg)) syncActionBridgeSourceFromRelay(seg);
syncSegmentTextPeers(seg.id, "prompt", caption.value, caption);
syncSegmentRelayPeers(seg.id, Boolean(seg.use_prompt), null);
writeTimeline();
writeTimeline({ force: true });
logPromptPersistence(seg, "timeline_caption_input");
};
caption.onchange = flushTimelineWrite;
caption.onblur = flushTimelineWrite;
caption.onkeyup = () => {
markPromptFieldEdited(caption);
writeTimeline({ force: true });
logPromptPersistence(seg, "timeline_caption_keyup");
};
caption.onpaste = () => setTimeout(() => {
markPromptFieldEdited(caption);
seg.prompt = caption.value;
seg.use_prompt = Boolean(String(caption.value || "").trim());
syncSegmentTextPeers(seg.id, "prompt", caption.value, caption);
syncSegmentRelayPeers(seg.id, Boolean(seg.use_prompt), null);
writeTimeline({ force: true });
logPromptPersistence(seg, "timeline_caption_paste");
}, 0);
caption.oncompositionend = () => {
markPromptFieldEdited(caption);
seg.prompt = caption.value;
seg.use_prompt = Boolean(String(caption.value || "").trim());
syncSegmentTextPeers(seg.id, "prompt", caption.value, caption);
syncSegmentRelayPeers(seg.id, Boolean(seg.use_prompt), null);
writeTimeline({ force: true });
logPromptPersistence(seg, "timeline_caption_compositionend");
};
protectControlDrag(caption);
block.appendChild(caption);
}
@@ -7922,6 +8168,7 @@ function renderShotboardV3(node) {
input.dataset.iamccsV3Key = String(key);
input.style.cssText = inputBase() + `background:${purple.valueBg};border-color:${purple.border};color:${purple.valueText};font-weight:${type === "textarea" ? "700" : "800"};text-align:${type === "textarea" ? "left" : "center"};${type === "textarea" ? `min-height:54px;resize:vertical;font-size:${promptFontSize(11)};` : "height:28px;"}`;
input.oninput = () => {
if (key === "prompt") markPromptFieldEdited(input);
let shouldRedraw = false;
if (key === "start") {
const nextStart = Math.max(0, Math.round(Number(input.value || 0)));
@@ -7957,11 +8204,36 @@ function renderShotboardV3(node) {
}
if (type === "textarea") syncSegmentTextPeers(seg.id, key, input.value, input);
}
writeTimeline();
writeTimeline(key === "prompt" ? { force: true } : {});
if (key === "prompt") logPromptPersistence(seg, "inspector_prompt_input");
if (shouldRedraw) draw();
};
input.onchange = flushTimelineWrite;
input.onblur = flushTimelineWrite;
if (key === "prompt") {
input.onkeyup = () => {
markPromptFieldEdited(input);
writeTimeline({ force: true });
};
input.onpaste = () => setTimeout(() => {
markPromptFieldEdited(input);
seg[key] = input.value;
seg.use_prompt = Boolean(String(input.value || "").trim());
syncSegmentTextPeers(seg.id, key, input.value, input);
syncSegmentRelayPeers(seg.id, Boolean(seg.use_prompt), null);
writeTimeline({ force: true });
logPromptPersistence(seg, "inspector_prompt_paste");
}, 0);
input.oncompositionend = () => {
markPromptFieldEdited(input);
seg[key] = input.value;
seg.use_prompt = Boolean(String(input.value || "").trim());
syncSegmentTextPeers(seg.id, key, input.value, input);
syncSegmentRelayPeers(seg.id, Boolean(seg.use_prompt), null);
writeTimeline({ force: true });
logPromptPersistence(seg, "inspector_prompt_compositionend");
};
}
protectControlDrag(input);
wrap.append(span, input);
return wrap;
@@ -8291,6 +8563,7 @@ function renderShotboardV3(node) {
prompt.style.cssText = `width:100%;height:68px;min-height:68px;box-sizing:border-box;padding:7px 9px;background:${purple.valueBg};border:1px solid rgba(143,208,204,.44);border-radius:5px;color:${purple.valueText};font:${promptFontSize(10)}/1.26 monospace;font-weight:700;outline:none;resize:none;overflow-y:auto;box-shadow:inset 0 1px 0 rgba(255,255,255,.56);`;
prompt.onpointerdown = (event) => event.stopPropagation();
prompt.oninput = () => {
markPromptFieldEdited(prompt);
seg.prompt = prompt.value;
seg.note = prompt.value;
seg.use_prompt = Boolean(String(prompt.value || "").trim());
@@ -8406,7 +8679,7 @@ function renderShotboardV3(node) {
relayStatus.style.gridRow = "1";
relayStatus.style.alignSelf = "end";
const dialogueHint = document.createElement("span");
const looksDialogue = /["“”]|\bsays?\b|\bspeak\b|\bdialog/i.test(String(seg.prompt || ""));
const looksDialogue = /["“”]|\bsays?\b|\bspeak\b|\bdialog/i.test(String(seg.prompt || ""));
dialogueHint.textContent = seg.dialogue_pin ? "PIN" : looksDialogue ? "DIALOG" : "PROMPT";
dialogueHint.title = seg.dialogue_pin ? "Dialogue pin is active for this box" : looksDialogue ? "This prompt appears to contain dialogue timing or spoken text" : "No obvious dialogue marker detected";
dialogueHint.style.cssText = `display:flex;align-items:center;justify-content:center;min-width:44px;padding:2px 5px;border-radius:999px;border:1px solid ${looksDialogue || seg.dialogue_pin ? "rgba(223,164,81,.62)" : purple.borderSoft};background:${looksDialogue || seg.dialogue_pin ? "rgba(96,64,34,.28)" : "rgba(0,0,0,.10)"};color:${looksDialogue || seg.dialogue_pin ? "#F4D49E" : purple.muted};font-size:7px;font-weight:900;line-height:1;white-space:nowrap;`;
@@ -9001,9 +9274,13 @@ function renderShotboardV3(node) {
for (const [name, value] of Object.entries(settingsData)) {
if (getWidget(node, name)) setWidgetValue(node, name, name === "image_resize_method" ? cineResizeMethodValue(value) : value);
}
for (const name of ["duration_seconds", "frame_rate", "default_force", "promptrelay_epsilon", "image_width", "image_height", "image_resize_method", "image_multiple_of", "img_compression", "guide_policy"]) {
for (const name of v3SettingNames) {
if (Object.prototype.hasOwnProperty.call(board, name) && getWidget(node, name)) setWidgetValue(node, name, name === "image_resize_method" ? cineResizeMethodValue(board[name]) : board[name]);
}
console.log("[IAMCCS V3 BOARD IMPORT] settings absorbed into node widgets", {
nodeId: node?.id,
settings: v3SettingsSnapshot(),
});
const paths = await packagedReferencePathsForImport(board, (message) => {
showTimelineNotice(message, /failed/i.test(String(message || "")) ? "error" : "warn");
});
@@ -9011,11 +9288,46 @@ function renderShotboardV3(node) {
else clearOwnReferencePaths(node);
let loadedTimeline = null;
let loadedTimelineSource = "";
const timelineText = String(board.timeline_data || data?.timeline_data || "").trim();
const timelineCandidates = [];
if (board.timeline && typeof board.timeline === "object") {
timelineCandidates.push({ source: "board.timeline", value: board.timeline });
}
if (timelineText) {
try { loadedTimeline = JSON.parse(timelineText); } catch {}
try { timelineCandidates.push({ source: "board.timeline_data", value: JSON.parse(timelineText) }); } catch {}
}
if (data?.timeline && typeof data.timeline === "object" && data.timeline !== board.timeline) {
timelineCandidates.push({ source: "root.timeline", value: data.timeline });
}
const scoreTimelineCandidate = (candidate) => {
const value = candidate?.value;
if (!value || typeof value !== "object") return -1;
const segments = Array.isArray(value.segments) ? value.segments : [];
const rows = Array.isArray(value.rows) ? value.rows : [];
const prompts = segments.length
? segments.filter((seg) => String(seg?.prompt || "").trim()).length
: rows.filter((row) => String(row?.relay_prompt ?? row?.local_prompt ?? row?.prompt ?? "").trim()).length;
const sourceBonus = candidate.source === "board.timeline" || candidate.source === "root.timeline" ? 1000 : 0;
return sourceBonus + segments.length * 20 + rows.length * 10 + prompts;
};
timelineCandidates.sort((a, b) => scoreTimelineCandidate(b) - scoreTimelineCandidate(a));
if (timelineCandidates.length) {
loadedTimeline = timelineCandidates[0].value;
loadedTimelineSource = timelineCandidates[0].source;
console.log("[IAMCCS V3 BOARD IMPORT]", {
selected: loadedTimelineSource,
candidates: timelineCandidates.map((candidate) => ({
source: candidate.source,
score: scoreTimelineCandidate(candidate),
segments: Array.isArray(candidate.value?.segments) ? candidate.value.segments.length : 0,
rows: Array.isArray(candidate.value?.rows) ? candidate.value.rows.length : 0,
firstPrompt: Array.isArray(candidate.value?.segments)
? String(candidate.value.segments.find((seg) => String(seg?.prompt || "").trim())?.prompt || "").slice(0, 220)
: String((candidate.value?.rows || []).find((row) => String(row?.relay_prompt ?? row?.local_prompt ?? row?.prompt ?? "").trim())?.relay_prompt || "").slice(0, 220),
})),
});
}
if (!loadedTimeline && board.timeline && typeof board.timeline === "object") loadedTimeline = board.timeline;
if (loadedTimeline && typeof loadedTimeline === "object" && Array.isArray(loadedTimeline.segments)) {
timeline = {
schema: "iamccs.cine.filmmaker_timeline",
@@ -9043,8 +9355,10 @@ function renderShotboardV3(node) {
};
}
}
showTimelineNotice(`Imported board timeline source: ${loadedTimelineSource || "rows/timeline_data"}`, "warn");
selectedId = timeline.segments?.[0]?.id || null;
writeTimeline();
clearV3BoardTransientState();
writeTimeline({ force: true });
draw();
}
importBoardBtn.onclick = () => boardInput.click();
@@ -9060,8 +9374,11 @@ function renderShotboardV3(node) {
}
};
root.addEventListener("dragover", (event) => {
if (Array.from(event.dataTransfer?.items || []).some((item) => item.kind === "file")) event.preventDefault();
});
if (!hasFileDrag(event)) return;
event.preventDefault();
event.stopPropagation();
if (event.dataTransfer) event.dataTransfer.dropEffect = "copy";
}, { capture: true });
root.addEventListener("drop", async (event) => {
const files = Array.from(event.dataTransfer?.files || []);
const file = files.find((item) => /\.json$/i.test(item.name || ""));
@@ -9069,6 +9386,7 @@ function renderShotboardV3(node) {
const imageFiles = files.filter((item) => String(item.type || "").startsWith("image/"));
if (!file && !audioFiles.length && !imageFiles.length) return;
event.preventDefault();
event.stopPropagation();
if (file) {
try {
await applyImportedBoard(await readJsonFile(file));
@@ -9085,7 +9403,7 @@ function renderShotboardV3(node) {
await uploadAudioFiles(audioFiles);
return;
}
});
}, { capture: true });
collapseBtn.onclick = () => {
collapsed = !collapsed;
node.properties = node.properties || {};
@@ -9097,23 +9415,26 @@ function renderShotboardV3(node) {
};
saveBtn.onclick = () => {
writeTimeline({ force: true });
const timelineText = getWidget(node, "timeline_data")?.value || "";
let timelinePayload = null;
try { timelinePayload = JSON.parse(timelineText || "{}"); } catch {}
const promptText = timelineText || JSON.stringify(timeline || {});
console.log("[IAMCCS V3 BOARD SAVE]", {
kind: "board",
nodeId: node?.id,
containsCoastline: /\bcoastline\b/i.test(promptText),
firstPrompt: String(timelinePayload?.segments?.[0]?.prompt || timelinePayload?.rows?.[0]?.relay_prompt || "").replace(/\s+/g, " ").slice(0, 220),
settings: v3SettingsSnapshot(),
});
const board = {
metadata: { schema: "iamccs.cine.filmmaker_board", schema_version: 1, saved_at: new Date().toISOString(), node_type: nodeClassName(node) },
global_prompt: promptArea.value,
timeline_data: getWidget(node, "timeline_data")?.value || "",
timeline_data: timelineText,
timeline: timelinePayload || JSON.parse(JSON.stringify(timeline || {})),
image_paths: refPaths(),
settings: {
duration_seconds: durationWidget?.value,
frame_rate: fpsWidget?.value,
default_force: defaultForceWidget?.value,
promptrelay_epsilon: getWidget(node, "promptrelay_epsilon")?.value,
image_width: imageWidthWidget?.value,
image_height: imageHeightWidget?.value,
image_resize_method: cineResizeMethodValue(getWidget(node, "image_resize_method")?.value),
image_multiple_of: getWidget(node, "image_multiple_of")?.value,
img_compression: getWidget(node, "img_compression")?.value,
},
settings: v3SettingsSnapshot(),
};
Object.assign(board, board.settings);
downloadJsonFile(board, safeBoardFilename("cine_filmmaker_v3"));
};
savePackageBtn.onclick = async () => {
@@ -9121,24 +9442,25 @@ function renderShotboardV3(node) {
savePackageBtn.disabled = true;
writeTimeline({ force: true });
const timelineText = getWidget(node, "timeline_data")?.value || "";
let timelinePayload = null;
try { timelinePayload = JSON.parse(timelineText || "{}"); } catch {}
const promptText = timelineText || JSON.stringify(timeline || {});
console.log("[IAMCCS V3 BOARD SAVE]", {
kind: "package",
nodeId: node?.id,
containsCoastline: /\bcoastline\b/i.test(promptText),
firstPrompt: String(timelinePayload?.segments?.[0]?.prompt || timelinePayload?.rows?.[0]?.relay_prompt || "").replace(/\s+/g, " ").slice(0, 220),
settings: v3SettingsSnapshot(),
});
const board = {
metadata: { schema: "iamccs.cine.filmmaker_board", schema_version: 1, saved_at: new Date().toISOString(), node_type: nodeClassName(node) },
global_prompt: promptArea.value,
timeline_data: timelineText,
timeline,
timeline: timelinePayload || JSON.parse(JSON.stringify(timeline || {})),
image_paths: refPaths(),
settings: {
duration_seconds: durationWidget?.value,
frame_rate: fpsWidget?.value,
default_force: defaultForceWidget?.value,
promptrelay_epsilon: getWidget(node, "promptrelay_epsilon")?.value,
image_width: imageWidthWidget?.value,
image_height: imageHeightWidget?.value,
image_resize_method: cineResizeMethodValue(getWidget(node, "image_resize_method")?.value),
image_multiple_of: getWidget(node, "image_multiple_of")?.value,
img_compression: getWidget(node, "img_compression")?.value,
},
settings: v3SettingsSnapshot(),
};
Object.assign(board, board.settings);
await saveShotboardPackageFolder(board, "cine_filmmaker_v3", (message) => {
showTimelineNotice(message, message && /failed/i.test(message) ? "error" : "warn");
});
@@ -9174,6 +9496,27 @@ function renderShotboardV3(node) {
lockWidth: true,
preferredSize: [v3RigidWidth, currentNodeHeight()],
});
if (!node._iamccsCineShotboardV3SerializeWrapped) {
node._iamccsCineShotboardV3SerializeWrapped = true;
const originalOnSerialize = node.onSerialize;
node.onSerialize = function (serialized) {
const result = originalOnSerialize?.call?.(this, serialized);
try {
if (node._iamccsCineShotboardV3WriteTimeline) node._iamccsCineShotboardV3WriteTimeline({ force: true });
const timelineValue = String(getWidget(node, "timeline_data")?.value || "");
const globalValue = String(getWidget(node, "global_prompt")?.value || "");
syncSerializedWidgetValue(node, serialized, "timeline_data", timelineValue);
syncSerializedWidgetValue(node, serialized, "global_prompt", globalValue);
serialized.properties = serialized.properties || {};
serialized.properties.iamccs_v3_timeline_data_backup = timelineValue;
serialized.properties.iamccs_v3_global_prompt_backup = globalValue;
} catch (err) {
console.warn("[IAMCCS Cine Shotboard V3] timeline serialize flush failed", err);
}
return result;
};
}
node._iamccsCineShotboardV3WriteTimeline = writeTimeline;
setTimeout(draw, 0);
}
@@ -10324,8 +10667,55 @@ function scheduleRender(node) {
setTimeout(() => renderForNode(node), 450);
}
function flushAllShotboardV3Timelines(reason = "flush") {
const nodes = Array.isArray(app.graph?._nodes) ? app.graph._nodes : [];
let flushed = 0;
for (const node of nodes) {
if (nodeClassName(node) !== "IAMCCS_CineShotboardPlannerV3") continue;
if (typeof node._iamccsCineShotboardV3WriteTimeline !== "function") continue;
try {
node._iamccsCineShotboardV3WriteTimeline({ force: true });
flushed += 1;
const text = String(getWidget(node, "timeline_data")?.value || "");
let firstPrompt = "";
let containsCoastline = false;
try {
const data = JSON.parse(text || "{}");
firstPrompt = String(data?.segments?.[0]?.prompt || data?.rows?.[0]?.relay_prompt || "");
containsCoastline = /\bcoastline\b/i.test(text);
} catch {}
console.log("[IAMCCS V3 QUEUE FLUSH]", {
reason,
nodeId: node?.id,
timelineLength: text.length,
containsCoastline,
firstPrompt: firstPrompt.replace(/\s+/g, " ").slice(0, 220),
});
} catch (err) {
console.warn("[IAMCCS V3 QUEUE FLUSH] failed", { reason, nodeId: node?.id, err });
}
}
return flushed;
}
function wrapQueueFlush(target, methodName, label) {
if (!target || typeof target[methodName] !== "function") return;
const guard = `_iamccsCineV3QueueFlushWrapped_${methodName}`;
if (target[guard]) return;
target[guard] = true;
const original = target[methodName];
target[methodName] = function (...args) {
flushAllShotboardV3Timelines(label);
return original.apply(this, args);
};
}
app.registerExtension({
name: "iamccs.cine.timeline.ui",
async setup() {
wrapQueueFlush(api, "queuePrompt", "api.queuePrompt");
wrapQueueFlush(app, "queuePrompt", "app.queuePrompt");
},
async beforeRegisterNodeDef(nodeType, nodeData) {
const name = String(nodeData?.name || nodeData?.class_type || "");
if (name !== "IAMCCS_CineLTXSequencer" && name !== "IAMCCS_CinePromptRelayLatentShapeSync" && name !== "IAMCCS_CinePromptRelayTimeline" && name !== "IAMCCS_CineShotboardLite" && name !== "IAMCCS_CineShotboardTimelinePro" && name !== "IAMCCS_CineShotboardPlannerPro" && name !== "IAMCCS_CineShotboardPlannerProV2" && name !== "IAMCCS_CineShotboardPlannerV3" && name !== "IAMCCS_CineShotboardPlannerProLegacy" && name !== "IAMCCS_CineFLFEngineSimple" && name !== "IAMCCS_CineInfo" && name !== "IAMCCS_CinePromptArchitect" && name !== "IAMCCS_BoardMaker" && name !== "IAMCCS_CineMusicVideoPlanner") return;
@@ -10343,8 +10733,8 @@ app.registerExtension({
},
});
// ── NarrativePlanner push listener ───────────────────────────────────────────
// When the NarrativePlanner "→ Push to PlannerPro" button fires, re-render
// ── NarrativePlanner push listener ───────────────────────────────────────────
// When the NarrativePlanner "→ Push to PlannerPro" button fires, re-render
// the shotboard so the new rows appear immediately without a page reload.
// By Carmine Cristallo Scalzi AI research (IAMCCS) - patreon.com/IAMCCS - carminecristalloscalzi.com
document.addEventListener("iamccs:planner_rows_updated", (ev) => {
@@ -10362,3 +10752,5 @@ document.addEventListener("iamccs:planner_rows_updated", (ev) => {
plannerNode._iamccsCineShotboardV3Version = "";
scheduleRender(plannerNode);
});
+3 -3
View File
@@ -18,7 +18,7 @@ const PRESETS = {
},
"12GB": {
preview: { temporal_tile_size: 40, temporal_overlap: 16, guiding_strength: 0.90, temporal_overlap_cond_strength: 0.45, cond_image_strength: 1.0 },
balanced: { temporal_tile_size: 48, temporal_overlap: 16, guiding_strength: 1.00, temporal_overlap_cond_strength: 0.50, cond_image_strength: 1.0 },
balanced: { temporal_tile_size: 56, temporal_overlap: 24, guiding_strength: 1.00, temporal_overlap_cond_strength: 0.50, cond_image_strength: 1.0 },
quality: { temporal_tile_size: 56, temporal_overlap: 24, guiding_strength: 1.00, temporal_overlap_cond_strength: 0.55, cond_image_strength: 1.0 },
},
"16GB": {
@@ -172,8 +172,8 @@ function chooseAutoValues(hw, ctx, quality = "balanced", fallbackVram = "12GB")
base.spatial_overlap = 1;
base.spatial_tiling_mode = "force_off";
if (vram === "12GB") {
base.temporal_tile_size = Math.min(base.temporal_tile_size, 48);
base.temporal_overlap = Math.min(base.temporal_overlap, 16);
base.temporal_tile_size = Math.min(base.temporal_tile_size, 56);
base.temporal_overlap = Math.min(base.temporal_overlap, 24);
}
if (vram === "8GB") {
base.temporal_tile_size = Math.min(base.temporal_tile_size, 32);