diff --git a/js/msr_character.js b/js/msr_character.js
index 931ca61..581fa65 100644
--- a/js/msr_character.js
+++ b/js/msr_character.js
@@ -92,6 +92,9 @@ function setWidgetValue(node, widget, value) {
function syncFormFromWidgets(node) {
const description = findWidget(node, "description")?.value || "";
const alias = findWidget(node, "alias")?.value || "";
+ const provider = findWidget(node, "analyze_provider")?.value || "ollama";
+ const model = findWidget(node, "analyze_model")?.value || "";
+ const baseUrl = findWidget(node, "analyze_base_url")?.value || "";
if (node._msrAliasInput && node._msrAliasInput.value !== alias) {
node._msrAliasInput.value = alias;
@@ -104,6 +107,7 @@ function syncFormFromWidgets(node) {
alias
? `Alias: @${String(alias).replace(/^@/, "")}`
: "Alias: none",
+ `
Analyze: ${provider}${model ? ` / ${model}` : ""}${baseUrl ? ` / ${baseUrl}` : ""}
`,
description
? `${description}
`
: `Description will be used for prompt replacement outside MSR prefix mode.
`,
@@ -189,6 +193,36 @@ function buildCharacterUi(node) {
gap: "6px",
});
+ const settingsButton = document.createElement("button");
+ settingsButton.type = "button";
+ settingsButton.textContent = "Analyze Settings";
+ Object.assign(settingsButton.style, {
+ alignSelf: "flex-start",
+ background: "#252525",
+ color: "#ddd",
+ border: "1px solid #444",
+ borderRadius: "6px",
+ padding: "6px 10px",
+ fontSize: "11px",
+ cursor: "pointer",
+ });
+ settingsButton.addEventListener("click", () => {
+ const providerWidget = findWidget(node, "analyze_provider");
+ const baseUrlWidget = findWidget(node, "analyze_base_url");
+ const modelWidget = findWidget(node, "analyze_model");
+ const provider = window.prompt("Analyze provider: ollama, lmstudio, custom, off", providerWidget?.value || "ollama");
+ if (provider == null) return;
+ const baseUrl = window.prompt("Analyze base URL (blank = default)", baseUrlWidget?.value || "");
+ if (baseUrl == null) return;
+ const model = window.prompt("Analyze model (blank = provider default)", modelWidget?.value || "");
+ if (model == null) return;
+ setWidgetValue(node, providerWidget, provider.trim() || "ollama");
+ setWidgetValue(node, baseUrlWidget, baseUrl.trim());
+ setWidgetValue(node, modelWidget, model.trim());
+ syncFormFromWidgets(node);
+ node.setDirtyCanvas?.(true, true);
+ });
+
const aliasLabel = document.createElement("label");
aliasLabel.textContent = "Alias";
Object.assign(aliasLabel.style, {
@@ -245,6 +279,7 @@ function buildCharacterUi(node) {
form.appendChild(aliasInput);
form.appendChild(descLabel);
form.appendChild(descInput);
+ form.appendChild(settingsButton);
container.appendChild(previewRow);
container.appendChild(meta);
@@ -261,6 +296,9 @@ function buildCharacterUi(node) {
async function analyzeCharacterNode(node, buttonWidget) {
const descriptionWidget = findWidget(node, "description");
if (!descriptionWidget) return;
+ const providerWidget = findWidget(node, "analyze_provider");
+ const baseUrlWidget = findWidget(node, "analyze_base_url");
+ const modelWidget = findWidget(node, "analyze_model");
buttonWidget.label = "Analyzing...";
node.setDirtyCanvas?.(true, true);
@@ -280,7 +318,9 @@ async function analyzeCharacterNode(node, buttonWidget) {
const response = await api.fetchApi("/ltx_director/analyze_character", {
method: "POST",
body: JSON.stringify({
- provider: "ollama",
+ provider: providerWidget?.value || "ollama",
+ base_url: baseUrlWidget?.value || "",
+ model: modelWidget?.value || "",
image_b64: images,
}),
});
@@ -332,6 +372,18 @@ app.registerExtension({
buttonWidget.label = "Analyze with Ollama";
}
+ if (!findWidget(this, "analyze_provider")) {
+ this.addWidget("combo", "analyze_provider", "ollama", null, {
+ values: ["ollama", "lmstudio", "custom", "off"],
+ });
+ }
+ if (!findWidget(this, "analyze_base_url")) {
+ this.addWidget("text", "analyze_base_url", "");
+ }
+ if (!findWidget(this, "analyze_model")) {
+ this.addWidget("text", "analyze_model", "");
+ }
+
if (!this._msrPreviewWidget) {
const previewContainer = buildCharacterUi(this);
this._msrPreviewWidget = this.addDOMWidget("msr_character_ui", "msr_character_ui", previewContainer, {
@@ -358,7 +410,7 @@ app.registerExtension({
return result;
};
- ["alias", "description"].forEach((widgetName) => {
+ ["alias", "description", "analyze_provider", "analyze_base_url", "analyze_model"].forEach((widgetName) => {
hideNodeWidget(findWidget(this, widgetName));
});
diff --git a/ltx_director.py b/ltx_director.py
index baf8c17..83dec6a 100644
--- a/ltx_director.py
+++ b/ltx_director.py
@@ -445,6 +445,27 @@ _ANALYZE_PROMPT = (
)
+def _extract_ollama_generated_text(resp_json: dict) -> str:
+ if not isinstance(resp_json, dict):
+ return ""
+
+ candidates = [resp_json.get("response"), resp_json.get("content"), resp_json.get("thinking")]
+ message = resp_json.get("message")
+ if isinstance(message, dict):
+ candidates.extend([
+ message.get("content"),
+ message.get("reasoning_content"),
+ message.get("thinking"),
+ ])
+
+ for candidate in candidates:
+ if isinstance(candidate, str):
+ text = candidate.strip()
+ if text:
+ return text
+ return ""
+
+
def _compress_analysis_image_b64(b64_payload: str, max_dim: int = 768, quality: int = 82) -> str:
"""Shrink analysis images so multimodal providers do not burn their full context on pixels."""
try:
@@ -531,12 +552,12 @@ async def analyze_character_endpoint(request):
retry_err = await retry_response.text()
return web.json_response({"status": "error", "message": f"Ollama HTTP {retry_response.status}: {retry_err}"})
resp_json = await retry_response.json()
- generated_text = (resp_json.get("response") or "").strip()
+ generated_text = _extract_ollama_generated_text(resp_json)
else:
return web.json_response({"status": "error", "message": f"Ollama HTTP {response.status}: {err_txt}"})
else:
resp_json = await response.json()
- generated_text = (resp_json.get("response") or "").strip()
+ generated_text = _extract_ollama_generated_text(resp_json)
else:
# OpenAI-compatible vision chat (LM Studio / Custom).
content = [{"type": "text", "text": _ANALYZE_PROMPT}]