diff --git a/iamccs_ltx2_extension_module.py b/iamccs_ltx2_extension_module.py index 0654732..4bd4029 100644 --- a/iamccs_ltx2_extension_module.py +++ b/iamccs_ltx2_extension_module.py @@ -136,6 +136,7 @@ class IAMCCS_LTX2_ExtensionModule: # Default is 'custom' to preserve existing workflows. "preset": ([ "custom", + "target_extension_ltx2", "cut_bestofk_16", "cut_bestofk_16_luma", "cut_bestofk_32", diff --git a/web/iamccs_ltx2_extension_presets.js b/web/iamccs_ltx2_extension_presets.js index 13d961a..9bd9a4c 100644 --- a/web/iamccs_ltx2_extension_presets.js +++ b/web/iamccs_ltx2_extension_presets.js @@ -1,8 +1,41 @@ import { app } from "../../scripts/app.js"; -console.log("[IAMCCS LTX2] Loading ExtensionModule presets..."); +const IAMCCS_LTX2_EXTENSION_PRESETS_VERSION = "2026-03-01-1"; + +console.log( + `[IAMCCS LTX2] Loading ExtensionModule presets... v=${IAMCCS_LTX2_EXTENSION_PRESETS_VERSION}` +); const PRESETS = { + // Target workflow parity: matches `tarket extension ltx2.json` stitch settings + // - overlap = 10 + // - overlap_side = source + // - overlap_mode = linear_blend + // - start_images = last (overlap-1) frames from the overlap window + target_extension_ltx2: { + overlap_frames: 10, + overlap_mode: "linear_blend", + overlap_side: "source", + + // Match the original graph math: num_frames = overlap - 1 + enable_math: true, + math_operation: "a-1", + math_value_b: 1, + safe_mode: "none", + start_frames_rule: "none", + + // Keep quality upgrades disabled (parity) + seam_search_mode: "none", + k_search: 0, + color_match_mode: "none", + color_match_strength: 0.0, + color_reference_window: 8, + + // Keep defaults explicit for stability + metric_weight_color: 1.0, + metric_weight_edges: 0.5, + }, + // Prova 1: cut seam + best_of_k (no crossfade) cut_bestofk_16: { overlap_frames: 10, @@ -52,6 +85,8 @@ const PRESETS = { }, }; +console.log(`[IAMCCS LTX2] Extension presets keys: ${Object.keys(PRESETS).join(", ")}`); + function getWidget(node, name) { return node?.widgets?.find((w) => w?.name === name); } @@ -110,51 +145,92 @@ function hideWidget(widget) { } } -function ensureTopPresetWidget(node) { - // Create a visible preset control at the top, without changing serialization order. - // This avoids breaking older workflows that rely on widgets_values ordering. - const existingTop = getWidget(node, "preset_ui"); - if (existingTop) return; +const PRESET_UI_WIDGET = "preset_ui"; - const backendPreset = getWidget(node, "preset"); - node.properties = node.properties || {}; - const saved = node.properties.iamccs_extension_preset; - const initial = backendPreset?.value ?? saved ?? "custom"; - const values = [ - "custom", - "cut_bestofk_16", - "cut_bestofk_16_luma", - "cut_bestofk_32", - "micro_crossfade_3", - ]; +const ALLOWED_PRESET_KEYS = [ + "custom", + "target_extension_ltx2", + "cut_bestofk_16", + "cut_bestofk_16_luma", + "cut_bestofk_32", + "micro_crossfade_3", +]; - // Insert widget at the very top. - const w = node.addWidget( - "combo", - "Preset", - initial, - (v) => { - node.properties.iamccs_extension_preset = String(v || "custom"); - // Sync backend widget if present. - if (backendPreset) { - backendPreset.value = v; - try { - backendPreset.callback?.(v, app.canvas, node); - } catch { - // ignore - } - } - // Apply overrides (updates other widgets live) - applyPreset(node, v); - }, - { values } - ); - w.name = "preset_ui"; - w.serialize = false; +const ALLOWED_MATH_OPERATION = [ + "none", + "a-b", + "a-1", + "a+b", + "a*b", + "a/b", + "min(a,b)", + "max(a,b)", +]; - // Move it to index 0 in the widgets array (top of UI) +const ALLOWED_SAFE_MODE = ["none", "native_workflow_safe"]; + +function _coerceInt(value, fallback) { + const n = Number(value); + if (!Number.isFinite(n)) return fallback; + return Math.trunc(n); +} + +function _sanitizeNodeValues(node) { + // If a workflow was saved while widgets_values were index-shifted, + // ComfyUI may restore impossible values (e.g. strings into INT widgets). + // This causes prompt validation to fail and prevents the node from running. + let changed = false; + + const setCombo = (name, allowed, def) => { + const w = getWidget(node, name); + if (!w) return; + const v = String(w.value ?? ""); + if (!allowed.includes(v)) { + w.value = def; + changed = true; + } + }; + + const setInt = (name, def, min, max) => { + const w = getWidget(node, name); + if (!w) return; + const n = _coerceInt(w.value, def); + const clamped = Math.max(min, Math.min(max, n)); + if (w.value !== clamped) { + w.value = clamped; + changed = true; + } + }; + + // Fix the exact fields seen in prompt validation errors. + setInt("math_value_b", 1, 0, 256); + setCombo("math_operation", ALLOWED_MATH_OPERATION, "a-b"); + setCombo("safe_mode", ALLOWED_SAFE_MODE, "none"); + setCombo("preset", ALLOWED_PRESET_KEYS, "custom"); + + if (changed) { + try { + node.setDirtyCanvas(true, true); + } catch { + // ignore + } + } +} + +function _removePresetUiWidget(node) { + const idx = getWidgetIndex(node, PRESET_UI_WIDGET); + if (idx < 0) return null; try { - const idx = getWidgetIndex(node, "preset_ui"); + const [item] = node.widgets.splice(idx, 1); + return item || null; + } catch { + return null; + } +} + +function _movePresetUiWidgetToTop(node) { + try { + const idx = getWidgetIndex(node, PRESET_UI_WIDGET); if (idx > 0) { const [item] = node.widgets.splice(idx, 1); node.widgets.unshift(item); @@ -162,12 +238,58 @@ function ensureTopPresetWidget(node) { } catch { // ignore } +} + +function _createPresetUiWidgetAtEnd(node) { + const existing = getWidget(node, PRESET_UI_WIDGET); + if (existing) return existing; + + const backendPreset = getWidget(node, "preset"); + node.properties = node.properties || {}; + const saved = node.properties.iamccs_extension_preset; + const initial = String(backendPreset?.value ?? saved ?? "custom"); + const values = [ + "custom", + "target_extension_ltx2", + "cut_bestofk_16", + "cut_bestofk_16_luma", + "cut_bestofk_32", + "micro_crossfade_3", + ]; + + const w = node.addWidget( + "combo", + "Preset", + initial, + (v) => { + const key = String(v || "custom"); + node.properties = node.properties || {}; + node.properties.iamccs_extension_preset = key; + + // Sync backend widget if present. + const backend = getWidget(node, "preset"); + if (backend) { + backend.value = key; + try { + backend.callback?.(key, app.canvas, node); + } catch { + // ignore + } + } + + // Apply overrides (updates other widgets live) + applyPreset(node, key); + }, + { values } + ); + + w.name = PRESET_UI_WIDGET; + w.serialize = false; // Hide the backend preset widget if it exists (avoid duplicate controls). if (backendPreset) hideWidget(backendPreset); - // Apply immediately for loaded workflows. - applyPreset(node, w.value); + return w; } app.registerExtension({ @@ -177,13 +299,72 @@ app.registerExtension({ const nodeName = nodeData?.name; if (nodeName !== "IAMCCS_LTX2_ExtensionModule" && nodeName !== "IAMCCS_LTX2_ExtensionModule_simple") return; + const serialize = nodeType.prototype.serialize; + nodeType.prototype.serialize = function () { + // Strip the UI-only widget while serializing to avoid any chance + // of widgets_values index shifting across ComfyUI/LiteGraph builds. + const node = this; + const removed = _removePresetUiWidget(node); + const r = serialize?.apply(this, arguments); + if (removed) { + node.widgets.push(removed); + _movePresetUiWidgetToTop(node); + } else { + _createPresetUiWidgetAtEnd(node); + _movePresetUiWidgetToTop(node); + } + return r; + }; + const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { const r = onNodeCreated?.apply(this, arguments); const node = this; - // Always install top preset widget (even if backend widget isn't present yet). - ensureTopPresetWidget(node); + // Create the UI-only preset widget at the end. + // DO NOT move to top here: on loaded workflows, configure() will run after onNodeCreated, + // and any widget index shift before widgets_values restore can corrupt persistence. + _removePresetUiWidget(node); + _createPresetUiWidgetAtEnd(node); + + // For brand-new nodes (no configure path), move it to top in a microtask. + queueMicrotask(() => { + try { + if (!node._iamccs_ltx2_did_configure) { + _movePresetUiWidgetToTop(node); + } + } catch { + // ignore + } + }); + + return r; + }; + + const onConfigure = nodeType.prototype.onConfigure; + nodeType.prototype.onConfigure = function () { + const r = onConfigure?.apply(this, arguments); + const node = this; + node._iamccs_ltx2_did_configure = true; + + // widgets_values have been restored at this point. + const backendPreset = getWidget(node, "preset"); + node.properties = node.properties || {}; + const restored = String(backendPreset?.value ?? node.properties.iamccs_extension_preset ?? "custom"); + node.properties.iamccs_extension_preset = restored; + + if (backendPreset) backendPreset.value = restored; + + // Guard against previously-corrupted widgets_values causing invalid prompt inputs. + _sanitizeNodeValues(node); + + _removePresetUiWidget(node); + const ui = _createPresetUiWidgetAtEnd(node); + ui.value = restored; + _movePresetUiWidgetToTop(node); + + // Apply preset AFTER restore so UI reflects backend-enforced behavior. + applyPreset(node, restored); return r; };