From 67696a4e59161e03f34d5198da3bcead4c5b153b Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Fri, 10 Jul 2026 09:46:36 +0000 Subject: [PATCH] Remove legacy director MSR character path --- js/ltx_director.js | 486 +++------------------------------------------ ltx_director.py | 30 --- 2 files changed, 33 insertions(+), 483 deletions(-) diff --git a/js/ltx_director.js b/js/ltx_director.js index 31a1b2a..d9023ee 100644 --- a/js/ltx_director.js +++ b/js/ltx_director.js @@ -782,13 +782,10 @@ function parseInitial(jsonStr) { retakeLength: 48, retakePrompt: "", retakeStrength: 1.0, - retakeVideo: null, - normalStartFrame: 0, - normalDurationFrames: 120, - reference_mode: "OFF", - analyzeProvider: "ollama", - analyzeBaseUrl: "", - analyzeModel: "" + retakeVideo: null, + normalStartFrame: 0, + normalDurationFrames: 120, + reference_mode: "OFF" }; try { if (jsonStr) { @@ -808,13 +805,10 @@ function parseInitial(jsonStr) { if (p.retakeLength !== undefined) parsed.retakeLength = p.retakeLength; if (p.retakePrompt !== undefined) parsed.retakePrompt = p.retakePrompt; if (p.retakeStrength !== undefined) parsed.retakeStrength = p.retakeStrength; - if (p.retakeVideo !== undefined) parsed.retakeVideo = p.retakeVideo; - if (p.normalStartFrame !== undefined) parsed.normalStartFrame = p.normalStartFrame; - if (p.normalDurationFrames !== undefined) parsed.normalDurationFrames = p.normalDurationFrames; + if (p.retakeVideo !== undefined) parsed.retakeVideo = p.retakeVideo; + if (p.normalStartFrame !== undefined) parsed.normalStartFrame = p.normalStartFrame; + if (p.normalDurationFrames !== undefined) parsed.normalDurationFrames = p.normalDurationFrames; if (p.reference_mode !== undefined) parsed.reference_mode = p.reference_mode; - if (p.analyzeProvider !== undefined) parsed.analyzeProvider = p.analyzeProvider; - if (p.analyzeBaseUrl !== undefined) parsed.analyzeBaseUrl = p.analyzeBaseUrl; - if (p.analyzeModel !== undefined) parsed.analyzeModel = p.analyzeModel; if (Array.isArray(p.segments)) { parsed.segments = p.segments.map(stripImageSegmentTransient); } @@ -9005,309 +8999,7 @@ class TimelineEditor { } } - // --- Backend Data Sync --- - // --- Visual Character Reference Slots --- - createCharacterSlots(parent) { - const container = document.createElement("div"); - container.className = "prcs-characters-container"; - - if (!this.timeline.characters) { - this.timeline.characters = [ - { images: [], description: "" }, - { images: [], description: "" }, - { images: [], description: "" } - ]; - } - - this.characterSlots = []; - - for (let i = 0; i < 3; i++) { - const slot = document.createElement("div"); - slot.className = "prcs-character-slot"; - slot.dataset.index = i; - - slot.addEventListener("dragover", (e) => { - e.preventDefault(); - e.stopPropagation(); - slot.classList.add("drag-over"); - }); - slot.addEventListener("dragleave", (e) => { - e.stopPropagation(); - slot.classList.remove("drag-over"); - }); - slot.addEventListener("drop", (e) => { - e.preventDefault(); - e.stopPropagation(); - slot.classList.remove("drag-over"); - if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { - Array.from(e.dataTransfer.files).forEach(f => this.handleCharacterImageUpload(f, i)); - } - }); - slot.addEventListener("click", (e) => { - if (e.target.closest(".prcs-character-delete") || - e.target.closest(".prcs-character-validate-btn") || - e.target.closest(".prcs-character-desc")) return; - - const fi = document.createElement("input"); - fi.type = "file"; - fi.accept = "image/*"; - fi.multiple = true; - fi.addEventListener("change", (ev) => { - if (ev.target.files) { - Array.from(ev.target.files).forEach(f => this.handleCharacterImageUpload(f, i)); - } - }); - fi.click(); - }); - - container.appendChild(slot); - this.characterSlots.push(slot); - } - - parent.appendChild(container); - this.charPanelContainer = container; - this.charPanelHeight = 150; - this.updateCharacterSlotsUI(); - } - - handleCharacterImageUpload(file, idx) { - if (!file.type.startsWith("image/")) return; - const reader = new FileReader(); - reader.onload = (e) => { - const imgObj = new Image(); - imgObj.onload = async () => { - const maxDim = 1920; - let w = imgObj.width; - let h = imgObj.height; - if (w > maxDim || h > maxDim) { - if (w > h) { h = Math.round((h * maxDim) / w); w = maxDim; } - else { w = Math.round((w * maxDim) / h); h = maxDim; } - } - const canvas = document.createElement("canvas"); - canvas.width = w; - canvas.height = h; - const ctx = canvas.getContext("2d"); - ctx.drawImage(imgObj, 0, 0, w, h); - - // Upload the downscaled reference to the input folder and store only its - // filename. Embedding base64 in the timeline bloats the saved workflow; the - // backend loads the file directly for both Analyze and Ghost/MSR. - let stored = null; - try { - const blob = await new Promise((res) => canvas.toBlob(res, "image/jpeg", 0.95)); - const base = (file.name || "ref").replace(/\.[^.]+$/, "").replace(/[^a-zA-Z0-9_-]/g, "_"); - const upName = `ltxref_${base}_${Date.now()}.jpg`; - const body = new FormData(); - body.append("image", new File([blob], upName, { type: "image/jpeg" })); - body.append("subfolder", "whatdreamscost"); - const resp = await api.fetchApi("/upload/image", { method: "POST", body }); - if (resp.status === 200) { - const data = await resp.json(); - const sf = data.subfolder || ""; - stored = { name: sf ? sf + "/" + data.name : data.name }; - } - } catch (err) { - console.error("[LTXDirector] ref upload failed, embedding b64 as fallback:", err); - } - if (!stored) { - stored = { b64: canvas.toDataURL("image/jpeg", 0.95), name: file.name }; - } - - if (!this.timeline.characters[idx].images) { - this.timeline.characters[idx].images = []; - } - if (this.timeline.characters[idx].images.length >= 2) { - this.timeline.characters[idx].images.shift(); - } - this.timeline.characters[idx].images.push(stored); - this.updateCharacterSlotsUI(); - this.commitChanges(); - }; - imgObj.src = e.target.result; - }; - reader.readAsDataURL(file); - } - - _refImageSrc(imgData) { - if (!imgData) return ""; - if (imgData.b64) return imgData.b64; // legacy embedded data / fallback - if (imgData.name) { - const parts = imgData.name.split("/"); - const fn = parts.pop(); - const sf = parts.join("/"); - return api.apiURL(`/view?filename=${encodeURIComponent(fn)}&type=input&subfolder=${encodeURIComponent(sf)}`); - } - return ""; - } - - async _refImageToB64(imgData) { - if (!imgData) return null; - if (imgData.b64) return imgData.b64; // legacy embedded data - const src = this._refImageSrc(imgData); - if (!src) return null; - try { - const resp = await fetch(src); - const blob = await resp.blob(); - return await new Promise((res) => { - const r = new FileReader(); - r.onloadend = () => res(r.result); - r.readAsDataURL(blob); - }); - } catch (err) { - console.error("[LTXDirector] could not load ref image for analyze:", err); - return null; - } - } - - updateCharacterSlotsUI() { - if (!this.characterSlots) return; - if (!this.timeline.characters) { - this.timeline.characters = [ - { images: [], description: "" }, - { images: [], description: "" }, - { images: [], description: "" } - ]; - } - - for (let i = 0; i < 3; i++) { - const slot = this.characterSlots[i]; - const data = this.timeline.characters[i] || { images: [], description: "" }; - slot.innerHTML = ""; - - if (data.images && data.images.length > 0) { - const previewsRow = document.createElement("div"); - previewsRow.className = "prcs-character-previews-row"; - - data.images.forEach((imgData, imgIdx) => { - const imgWrapper = document.createElement("div"); - imgWrapper.className = "prcs-character-preview-wrapper"; - - const img = document.createElement("img"); - img.className = "prcs-character-preview"; - img.src = this._refImageSrc(imgData); - imgWrapper.appendChild(img); - - const delBtn = document.createElement("button"); - delBtn.className = "prcs-character-delete"; - delBtn.innerHTML = ICONS.close; - delBtn.title = "Delete Image"; - delBtn.addEventListener("click", (e) => { - e.stopPropagation(); - if (this.timeline.characters[i] && this.timeline.characters[i].images) { - this.timeline.characters[i].images.splice(imgIdx, 1); - this.updateCharacterSlotsUI(); - this.commitChanges(); - } - }); - imgWrapper.appendChild(delBtn); - previewsRow.appendChild(imgWrapper); - }); - - const _provider = this.timeline.analyzeProvider || "ollama"; - if (_provider !== "off") { - const valBtn = document.createElement("button"); - valBtn.className = "prcs-character-validate-btn"; - valBtn.textContent = data.description ? "Re-Analyze" : "Analyze"; - valBtn.title = "Run multimodal analysis on the reference image(s)"; - valBtn.style.left = "50%"; - valBtn.style.transform = "translateX(-50%)"; - valBtn.addEventListener("click", (e) => { - e.stopPropagation(); - this.runGemmaAnalysis(i, valBtn); - }); - previewsRow.appendChild(valBtn); - } - - slot.appendChild(previewsRow); - - const descInput = document.createElement("textarea"); - descInput.className = "prcs-character-desc"; - descInput.value = data.description || ""; - descInput.placeholder = "manual description..."; - descInput.addEventListener("input", () => { - this.timeline.characters[i].description = descInput.value; - this.commitChanges(); - }); - descInput.addEventListener("click", (e) => { e.stopPropagation(); }); - slot.appendChild(descInput); - } else { - const label = document.createElement("div"); - label.className = "prcs-character-label"; - label.textContent = `@char${i + 1}`; - - const placeholder = document.createElement("div"); - placeholder.className = "prcs-character-placeholder"; - placeholder.innerHTML = `${ICONS.upload}
Drop Sheet`; - - slot.appendChild(label); - slot.appendChild(placeholder); - } - } - } - - async runGemmaAnalysis(idx, btn) { - if (btn.classList.contains("loading")) return; - - btn.classList.add("loading"); - btn.textContent = "Analyzing..."; - - let clip_name = ""; - try { - const inputs = this.node.inputs || []; - const clipLink = inputs.find(i => i.name === "clip")?.link; - if (clipLink) { - const linkInfo = window.app.graph.links[clipLink]; - if (linkInfo) { - const originNode = window.app.graph.getNodeById(linkInfo.origin_id); - if (originNode) { - const widgets = originNode.widgets || []; - const modelWidget = widgets.find(w => - w.name === "clip_name" || w.name === "clip_name_1" || - w.name === "clip_name_2" || w.name === "clip" || w.name === "model_name" - ); - if (modelWidget) clip_name = modelWidget.value; - } - } - } - } catch (e) { - console.warn("[LTXDirector] Could not traverse graph to find CLIPLoader name", e); - } - - const b64_images = (await Promise.all( - (this.timeline.characters[idx].images || []).map(img => this._refImageToB64(img)) - )).filter(Boolean); - - try { - const resp = await api.fetchApi("/ltx_director/analyze_character", { - method: "POST", - body: JSON.stringify({ - clip_name: clip_name, - image_b64: b64_images, - char_index: idx, - provider: this.timeline.analyzeProvider || "ollama", - base_url: this.timeline.analyzeBaseUrl || "", - model: this.timeline.analyzeModel || "", - }) - }); - const result = await resp.json(); - if (result.status === "success") { - this.timeline.characters[idx].description = result.description; - btn.textContent = "Success!"; - setTimeout(() => { this.updateCharacterSlotsUI(); this.commitChanges(); }, 1500); - } else { - alert("Analysis Error: " + result.message); - btn.classList.remove("loading"); - btn.textContent = "Analyze"; - } - } catch (err) { - console.error("[LTXDirector] analysis request failed", err); - alert("Request failed. Is your server running?"); - btn.classList.remove("loading"); - btn.textContent = "Analyze"; - } - } - - // --- @char auto-complete popup (attaches to a given textarea) --- + // --- @char auto-complete popup (attaches to a given textarea) --- setupAutocomplete(input) { if (!input || input._prAutocompleteAttached) return; input._prAutocompleteAttached = true; @@ -9538,13 +9230,10 @@ class TimelineEditor { imageFile: this.timeline.retakeVideo.imageFile, videoDurationFrames: this.timeline.retakeVideo.videoDurationFrames, fileSize: this.timeline.retakeVideo.fileSize, - } : null, - normalStartFrame: this.timeline.normalStartFrame, + } : null, + normalStartFrame: this.timeline.normalStartFrame, normalDurationFrames: this.timeline.normalDurationFrames, reference_mode: this.timeline.reference_mode || "OFF", - analyzeProvider: this.timeline.analyzeProvider || "ollama", - analyzeBaseUrl: this.timeline.analyzeBaseUrl || "", - analyzeModel: this.timeline.analyzeModel || "", segments: sortedSegments.map(stripImageSegmentTransient), motionSegments: (this.timeline.motionSegments || []).map(stripMotionSegmentTransient), audioSegments: (this.timeline.audioSegments || []).map(stripAudioSegmentTransient) @@ -11340,102 +11029,7 @@ class TimelineEditor { menu.appendChild(this._makeSettingRow("Workspace Folder", btnOpenFolder)); - // --- Spacer + Analyze Backend section --- - const provDivider = document.createElement("div"); - provDivider.className = "prcs-settings-divider"; - menu.appendChild(provDivider); - - const provTitle = document.createElement("div"); - provTitle.className = "prcs-settings-title"; - provTitle.style.marginBottom = "2px"; - provTitle.textContent = "Analyze Backend"; - menu.appendChild(provTitle); - - const PROVIDER_DEFAULTS = { - "off": { url: "", model: "" }, - "ollama": { url: "http://127.0.0.1:11434", model: "huihui_ai/qwen3.5-abliterated:2b" }, - "lmstudio": { url: "http://127.0.0.1:1234", model: "" }, - "custom": { url: "", model: "" }, - }; - - if (!this.timeline.analyzeProvider) this.timeline.analyzeProvider = "ollama"; - if (this.timeline.analyzeBaseUrl === undefined) this.timeline.analyzeBaseUrl = ""; - if (this.timeline.analyzeModel === undefined) this.timeline.analyzeModel = ""; - - const provSelect = document.createElement("select"); - provSelect.className = "prcs-settings-select"; - [ - { v: "off", label: "Off / Manual (no Analyze)" }, - { v: "ollama", label: "Ollama" }, - { v: "lmstudio", label: "LM Studio" }, - { v: "custom", label: "Custom (OpenAI-compatible)" }, - ].forEach(o => { - const opt = document.createElement("option"); - opt.value = o.v; - opt.textContent = o.label; - provSelect.appendChild(opt); - }); - provSelect.value = this.timeline.analyzeProvider; - - const urlInput = document.createElement("input"); - urlInput.type = "text"; - urlInput.className = "prcs-settings-input"; - urlInput.style.width = "150px"; - urlInput.style.textAlign = "left"; - - const modelInput = document.createElement("input"); - modelInput.type = "text"; - modelInput.className = "prcs-settings-input"; - modelInput.style.width = "150px"; - modelInput.style.textAlign = "left"; - - const urlRow = this._makeSettingRow("Base URL", urlInput); - const modelRow = this._makeSettingRow("Model", modelInput); - - const refreshProviderRows = () => { - const prov = this.timeline.analyzeProvider || "ollama"; - const defs = PROVIDER_DEFAULTS[prov] || PROVIDER_DEFAULTS.ollama; - urlInput.placeholder = defs.url || "http://your-server:port"; - modelInput.placeholder = defs.model || "your-loaded-model-name"; - urlInput.value = this.timeline.analyzeBaseUrl || ""; - modelInput.value = this.timeline.analyzeModel || ""; - const isOff = (prov === "off"); - urlRow.style.display = isOff ? "none" : ""; - modelRow.style.display = isOff ? "none" : ""; - }; - - provSelect.addEventListener("change", (e) => { - this.timeline.analyzeProvider = e.target.value; - this.timeline.analyzeBaseUrl = ""; - this.timeline.analyzeModel = ""; - refreshProviderRows(); - this.updateCharacterSlotsUI(); - this.commitChanges(true); - }); - urlInput.addEventListener("change", () => { - this.timeline.analyzeBaseUrl = urlInput.value.trim(); - this.commitChanges(true); - }); - modelInput.addEventListener("change", () => { - this.timeline.analyzeModel = modelInput.value.trim(); - this.commitChanges(true); - }); - - menu.appendChild(this._makeSettingRow("Provider", provSelect)); - menu.appendChild(urlRow); - menu.appendChild(modelRow); - - const provNote = document.createElement("div"); - provNote.style.fontSize = "9px"; - provNote.style.color = "#777"; - provNote.style.padding = "2px 4px 0"; - provNote.style.lineHeight = "1.3"; - provNote.textContent = "Off = type descriptions by hand. LM Studio / Custom: hard VRAM eviction depends on your server version; set a short JIT/auto-unload TTL there if it doesn't release."; - menu.appendChild(provNote); - - refreshProviderRows(); - - // Position the menu below the anchor button (pop down) + // Position the menu below the anchor button (pop down) document.body.appendChild(menu); const rect = anchorEl.getBoundingClientRect(); const menuW = menu.offsetWidth || 230; @@ -11753,42 +11347,28 @@ const APPENDED_WIDGET_DEFAULTS = [ ]; app.registerExtension({ - name: "LTXDirectorCS", - async setup() { - // On Run, ask the chosen analyze backend to release its model from VRAM so it doesn't - // compete with LTX generation. Only fires when an LTX Director is in the graph and its - // provider isn't "off". Fully tolerant: failures are swallowed so they never block a run. - if (app._ltxDirectorUnloadHookInstalled) return; - app._ltxDirectorUnloadHookInstalled = true; - const origQueuePrompt = app.queuePrompt; - app.queuePrompt = async function (...args) { - try { - const nodes = app.graph?._nodes || []; - const director = nodes.find(n => n && (n.comfyClass === "LTXDirectorCS" || n.type === "LTXDirectorCS")); - if (director) { - // Read provider settings from the node's saved timeline_data widget. - let provider = "ollama", baseUrl = "", model = ""; - try { - const tdWidget = director.widgets?.find(w => w.name === "timeline_data"); - if (tdWidget && tdWidget.value) { - const td = JSON.parse(tdWidget.value); - provider = td.analyzeProvider || "ollama"; - baseUrl = td.analyzeBaseUrl || ""; - model = td.analyzeModel || ""; - } - } catch (e) {} - if (provider !== "off") { - try { - await api.fetchApi("/ltx_director/unload_ollama", { - method: "POST", - body: JSON.stringify({ provider, base_url: baseUrl, model }), - }); - } catch (e) {} - } - } - } catch (e) {} - return origQueuePrompt.apply(this, args); - }; + name: "LTXDirectorCS", + async setup() { + // On Run, ask Ollama to release the analysis model from VRAM so it doesn't + // compete with LTX generation. Fully tolerant: failures are swallowed. + if (app._ltxDirectorUnloadHookInstalled) return; + app._ltxDirectorUnloadHookInstalled = true; + const origQueuePrompt = app.queuePrompt; + app.queuePrompt = async function (...args) { + try { + const nodes = app.graph?._nodes || []; + const director = nodes.find(n => n && (n.comfyClass === "LTXDirectorCS" || n.type === "LTXDirectorCS")); + if (director) { + try { + await api.fetchApi("/ltx_director/unload_ollama", { + method: "POST", + body: JSON.stringify({ provider: "ollama" }), + }); + } catch (e) {} + } + } catch (e) {} + return origQueuePrompt.apply(this, args); + }; }, async beforeRegisterNodeDef(nodeType, nodeData, app) { if (nodeData.name === "LTXDirectorCS") { diff --git a/ltx_director.py b/ltx_director.py index e491783..87dc5cf 100644 --- a/ltx_director.py +++ b/ltx_director.py @@ -1194,36 +1194,6 @@ def _extract_runtime_character_entries(tdata: dict, character_set, image_cache: except Exception as e: log.warning("[LTXDirector] Could not process external character_set input: %s", e) - if entries: - return entries - - try: - for char_info in tdata.get("characters", []): - images_list = char_info.get("images", []) - legacy_b64 = char_info.get("imageB64", "") - if legacy_b64 and not images_list: - images_list = [{"b64": legacy_b64, "name": char_info.get("fileName", "")}] - - loaded_images = [] - for img_info in images_list: - tensor = _load_image_source( - img_info.get("b64", ""), - img_info.get("name", ""), - cache=image_cache, - input_dir=input_dir, - ) - loaded_images.append(tensor) - - entries.append( - { - "images": loaded_images, - "description": char_info.get("description", "") or "", - "alias": _normalize_character_alias(char_info.get("alias", "")), - } - ) - except Exception as e: - log.warning("[LTXDirector] Could not process legacy timeline character slots: %s", e) - return entries