Fix Shotboard V3 persistence bugs for 1.4.6
This commit is contained in:
+95
-12
@@ -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 "
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user