Remove legacy director MSR character path
This commit is contained in:
@@ -785,10 +785,7 @@ function parseInitial(jsonStr) {
|
|||||||
retakeVideo: null,
|
retakeVideo: null,
|
||||||
normalStartFrame: 0,
|
normalStartFrame: 0,
|
||||||
normalDurationFrames: 120,
|
normalDurationFrames: 120,
|
||||||
reference_mode: "OFF",
|
reference_mode: "OFF"
|
||||||
analyzeProvider: "ollama",
|
|
||||||
analyzeBaseUrl: "",
|
|
||||||
analyzeModel: ""
|
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
if (jsonStr) {
|
if (jsonStr) {
|
||||||
@@ -812,9 +809,6 @@ function parseInitial(jsonStr) {
|
|||||||
if (p.normalStartFrame !== undefined) parsed.normalStartFrame = p.normalStartFrame;
|
if (p.normalStartFrame !== undefined) parsed.normalStartFrame = p.normalStartFrame;
|
||||||
if (p.normalDurationFrames !== undefined) parsed.normalDurationFrames = p.normalDurationFrames;
|
if (p.normalDurationFrames !== undefined) parsed.normalDurationFrames = p.normalDurationFrames;
|
||||||
if (p.reference_mode !== undefined) parsed.reference_mode = p.reference_mode;
|
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)) {
|
if (Array.isArray(p.segments)) {
|
||||||
parsed.segments = p.segments.map(stripImageSegmentTransient);
|
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) ---
|
// --- @char auto-complete popup (attaches to a given textarea) ---
|
||||||
setupAutocomplete(input) {
|
setupAutocomplete(input) {
|
||||||
if (!input || input._prAutocompleteAttached) return;
|
if (!input || input._prAutocompleteAttached) return;
|
||||||
@@ -9542,9 +9234,6 @@ class TimelineEditor {
|
|||||||
normalStartFrame: this.timeline.normalStartFrame,
|
normalStartFrame: this.timeline.normalStartFrame,
|
||||||
normalDurationFrames: this.timeline.normalDurationFrames,
|
normalDurationFrames: this.timeline.normalDurationFrames,
|
||||||
reference_mode: this.timeline.reference_mode || "OFF",
|
reference_mode: this.timeline.reference_mode || "OFF",
|
||||||
analyzeProvider: this.timeline.analyzeProvider || "ollama",
|
|
||||||
analyzeBaseUrl: this.timeline.analyzeBaseUrl || "",
|
|
||||||
analyzeModel: this.timeline.analyzeModel || "",
|
|
||||||
segments: sortedSegments.map(stripImageSegmentTransient),
|
segments: sortedSegments.map(stripImageSegmentTransient),
|
||||||
motionSegments: (this.timeline.motionSegments || []).map(stripMotionSegmentTransient),
|
motionSegments: (this.timeline.motionSegments || []).map(stripMotionSegmentTransient),
|
||||||
audioSegments: (this.timeline.audioSegments || []).map(stripAudioSegmentTransient)
|
audioSegments: (this.timeline.audioSegments || []).map(stripAudioSegmentTransient)
|
||||||
@@ -11340,101 +11029,6 @@ class TimelineEditor {
|
|||||||
|
|
||||||
menu.appendChild(this._makeSettingRow("Workspace Folder", btnOpenFolder));
|
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);
|
document.body.appendChild(menu);
|
||||||
const rect = anchorEl.getBoundingClientRect();
|
const rect = anchorEl.getBoundingClientRect();
|
||||||
@@ -11755,9 +11349,8 @@ const APPENDED_WIDGET_DEFAULTS = [
|
|||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "LTXDirectorCS",
|
name: "LTXDirectorCS",
|
||||||
async setup() {
|
async setup() {
|
||||||
// On Run, ask the chosen analyze backend to release its model from VRAM so it doesn't
|
// On Run, ask Ollama to release the analysis model from VRAM so it doesn't
|
||||||
// compete with LTX generation. Only fires when an LTX Director is in the graph and its
|
// compete with LTX generation. Fully tolerant: failures are swallowed.
|
||||||
// provider isn't "off". Fully tolerant: failures are swallowed so they never block a run.
|
|
||||||
if (app._ltxDirectorUnloadHookInstalled) return;
|
if (app._ltxDirectorUnloadHookInstalled) return;
|
||||||
app._ltxDirectorUnloadHookInstalled = true;
|
app._ltxDirectorUnloadHookInstalled = true;
|
||||||
const origQueuePrompt = app.queuePrompt;
|
const origQueuePrompt = app.queuePrompt;
|
||||||
@@ -11766,25 +11359,12 @@ app.registerExtension({
|
|||||||
const nodes = app.graph?._nodes || [];
|
const nodes = app.graph?._nodes || [];
|
||||||
const director = nodes.find(n => n && (n.comfyClass === "LTXDirectorCS" || n.type === "LTXDirectorCS"));
|
const director = nodes.find(n => n && (n.comfyClass === "LTXDirectorCS" || n.type === "LTXDirectorCS"));
|
||||||
if (director) {
|
if (director) {
|
||||||
// Read provider settings from the node's saved timeline_data widget.
|
|
||||||
let provider = "ollama", baseUrl = "", model = "";
|
|
||||||
try {
|
try {
|
||||||
const tdWidget = director.widgets?.find(w => w.name === "timeline_data");
|
await api.fetchApi("/ltx_director/unload_ollama", {
|
||||||
if (tdWidget && tdWidget.value) {
|
method: "POST",
|
||||||
const td = JSON.parse(tdWidget.value);
|
body: JSON.stringify({ provider: "ollama" }),
|
||||||
provider = td.analyzeProvider || "ollama";
|
});
|
||||||
baseUrl = td.analyzeBaseUrl || "";
|
|
||||||
model = td.analyzeModel || "";
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
} 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) {}
|
} catch (e) {}
|
||||||
return origQueuePrompt.apply(this, args);
|
return origQueuePrompt.apply(this, args);
|
||||||
|
|||||||
@@ -1194,36 +1194,6 @@ def _extract_runtime_character_entries(tdata: dict, character_set, image_cache:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning("[LTXDirector] Could not process external character_set input: %s", 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
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user