Remove legacy director MSR character path

This commit is contained in:
OpenClaw Agent
2026-07-10 09:46:36 +00:00
parent e68357f384
commit 67696a4e59
2 changed files with 33 additions and 483 deletions

View File

@@ -785,10 +785,7 @@ function parseInitial(jsonStr) {
retakeVideo: null,
normalStartFrame: 0,
normalDurationFrames: 120,
reference_mode: "OFF",
analyzeProvider: "ollama",
analyzeBaseUrl: "",
analyzeModel: ""
reference_mode: "OFF"
};
try {
if (jsonStr) {
@@ -812,9 +809,6 @@ function parseInitial(jsonStr) {
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,308 +8999,6 @@ 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}<br>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) ---
setupAutocomplete(input) {
if (!input || input._prAutocompleteAttached) return;
@@ -9542,9 +9234,6 @@ class TimelineEditor {
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,101 +11029,6 @@ 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)
document.body.appendChild(menu);
const rect = anchorEl.getBoundingClientRect();
@@ -11755,9 +11349,8 @@ 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.
// 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;
@@ -11766,25 +11359,12 @@ app.registerExtension({
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 || "";
}
await api.fetchApi("/ltx_director/unload_ollama", {
method: "POST",
body: JSON.stringify({ provider: "ollama" }),
});
} 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);

View File

@@ -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