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}]