import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; const TARGET = "AudioCppEngineNode"; const ENDPOINT = "/api/tts-audio-suite/audio-cpp-capabilities"; const STATUS_ENDPOINT = "/api/tts-audio-suite/audio-cpp-status"; const PANEL_HEIGHT = 210; const PANEL_MIN_WIDTH = 360; const PANEL_BOTTOM_PADDING = 14; const PANEL_LAYOUT_HEIGHT = PANEL_HEIGHT + PANEL_BOTTOM_PADDING; const REQUEST_ADVANCED_WIDGETS = [ "temperature", "top_p", "top_k", "repetition_penalty", "max_tokens", "max_steps", "num_inference_steps", "guidance_scale", "advanced_json", ]; const OWNED_ADVANCED_WIDGETS = [ "auto_download_runtime", "auto_download_model", "show_server_console", ]; let manifestPromise; function manifest() { manifestPromise ??= api.fetchApi(ENDPOINT).then((response) => { if (!response.ok) throw new Error(`Capability request failed (${response.status})`); return response.json(); }); return manifestPromise; } function widget(node, name) { return (node.widgets || []).find((item) => item.name === name); } function hideWidget(item) { if (!item || item.__ttsAudioCppHidden) return; item.__ttsAudioCppOriginalType = item.type; item.__ttsAudioCppOriginalComputeSize = item.computeSize; item.type = "hidden"; item.hidden = true; item.computeSize = () => [0, -4]; if (item.element) item.element.style.display = "none"; item.__ttsAudioCppHidden = true; } function showWidget(item) { if (!item || !item.__ttsAudioCppHidden) return; item.type = item.__ttsAudioCppOriginalType; item.computeSize = item.__ttsAudioCppOriginalComputeSize; item.hidden = false; if (item.element) item.element.style.display = ""; item.__ttsAudioCppHidden = false; } function setWidgetVisible(node, name, visible) { const item = widget(node, name); if (visible) showWidget(item); else hideWidget(item); } function resizeNodeToContent(node) { if (node.__ttsAudioCppResizeFrame) cancelAnimationFrame(node.__ttsAudioCppResizeFrame); node.__ttsAudioCppResizeFrame = requestAnimationFrame(() => { node.__ttsAudioCppResizeFrame = 0; const computed = node.computeSize(); const width = Math.max(Number(node.size?.[0]) || 0, PANEL_MIN_WIDTH, Number(computed?.[0]) || 0); const height = Number(computed?.[1]) || Number(node.size?.[1]) || PANEL_LAYOUT_HEIGHT; if (Math.abs(width - node.size[0]) > 0.5 || Math.abs(height - node.size[1]) > 0.5) { node.setSize([width, height]); } app.graph?.setDirtyCanvas(true, true); }); } function applyWidgetVisibility(node, capability) { if (!capability) return; const advanced = Boolean(node.__ttsAudioCppAdvancedOpen); const mode = String(widget(node, "connection_mode")?.value || "auto"); const backend = String(widget(node, "backend")?.value || "auto"); const external = mode === "external_server"; const existingBinary = mode === "existing_binary"; const suiteTasks = new Set(capability.suite_tasks || []); const upstreamTasks = new Set(capability.upstream_tasks || []); setWidgetVisible(node, "package_id", !external); setWidgetVisible(node, "task", external || upstreamTasks.size > 1 || advanced); setWidgetVisible(node, "backend", !external); setWidgetVisible(node, "device", !external && backend !== "cpu" && advanced); setWidgetVisible(node, "threads", !external && (backend === "cpu" || advanced)); setWidgetVisible(node, "language", suiteTasks.has("tts") || suiteTasks.has("asr")); setWidgetVisible(node, "server_url", external); setWidgetVisible(node, "binary_path", existingBinary || (!external && advanced)); setWidgetVisible(node, "model_path", existingBinary || (!external && advanced)); setWidgetVisible(node, "model_id", external); setWidgetVisible(node, "voice_id", Boolean(capability.built_in_voices)); setWidgetVisible(node, "instruct", Boolean(capability.voice_design)); for (const name of REQUEST_ADVANCED_WIDGETS) setWidgetVisible(node, name, advanced); for (const name of OWNED_ADVANCED_WIDGETS) setWidgetVisible(node, name, !external && advanced); const toggle = widget(node, "audio_cpp_advanced_toggle"); if (toggle) { toggle.label = advanced ? "▾ Hide advanced settings" : "▸ Show advanced settings"; } resizeNodeToContent(node); } function addAdvancedToggle(node) { const toggle = node.addWidget("button", "▸ Show advanced settings", null, () => { node.__ttsAudioCppAdvancedOpen = !node.__ttsAudioCppAdvancedOpen; applyWidgetVisibility(node, node.__ttsAudioCppCapability); }); toggle.name = "audio_cpp_advanced_toggle"; toggle.label = "▸ Show advanced settings"; toggle.options ??= {}; toggle.options.tooltip = "Show uncommon runtime paths, device tuning, sampling overrides, download controls, server debugging, and raw request JSON."; toggle.options.serialize = false; toggle.tooltip = toggle.options.tooltip; toggle.serialize = false; toggle.serializeValue = () => undefined; return toggle; } function formatBytes(bytes) { const value = Number(bytes); if (!Number.isFinite(value) || value <= 0) return "unavailable"; if (value >= 1073741824) return `${(value / 1073741824).toFixed(2)} GB`; return `${(value / 1048576).toFixed(1)} MB`; } function syncPackageChoices(node, data, capability) { const packageWidget = widget(node, "package_id"); if (!packageWidget || !capability) return null; const allowed = ["auto", ...(capability.packages || [])]; packageWidget.options ??= {}; packageWidget.options.values = allowed; if (!allowed.includes(String(packageWidget.value))) packageWidget.value = "auto"; const resolvedId = packageWidget.value === "auto" ? capability.recommended_package_id : String(packageWidget.value); return data.packages?.[resolvedId] || null; } function syncTaskChoices(node, capability) { const taskWidget = widget(node, "task"); if (!taskWidget || !capability) return; const allowed = ["auto", ...(capability.upstream_tasks || [])]; taskWidget.options ??= {}; taskWidget.options.values = [...new Set(allowed)]; if (!taskWidget.options.values.includes(String(taskWidget.value))) taskWidget.value = "auto"; } function normalizedUrl(value) { return String(value || "").trim().replace(/\/$/, ""); } function matchingStatus(node, sessions) { const family = String(widget(node, "family")?.value || ""); const modelId = String(widget(node, "model_id")?.value || "").trim(); const mode = String(widget(node, "connection_mode")?.value || "auto"); const endpoint = normalizedUrl(widget(node, "server_url")?.value); return (sessions || []).find((session) => { if (modelId && session.model_id !== modelId) return false; if (mode === "external_server" && endpoint) { return !session.owned && normalizedUrl(session.endpoint) === endpoint; } return session.family === family && (mode === "external_server" ? !session.owned : true); }); } function renderStatus(node, panel, sessions, failed = false) { const status = matchingStatus(node, sessions); const light = panel.querySelector(".tts-acpp-light"); const label = panel.querySelector(".tts-acpp-status-text"); const state = failed ? "error" : (status?.state || "unchecked"); const labels = { unchecked: "Not checked", configured: "Server stopped", server_ready: "Server connected · model not confirmed", model_ready: "Server connected · model ready", error: "Status unavailable", }; light.dataset.state = state; label.textContent = labels[state] || state; const memory = panel.querySelector(".tts-acpp-memory"); const stop = panel.querySelector(".tts-acpp-stop"); node.__ttsAudioCppSessionId = status?.session_id || ""; if (status?.pid) { const privateGb = status.private_bytes ? ` · ${(status.private_bytes / 1073741824).toFixed(1)} GB private` : ""; memory.textContent = `PID ${status.pid}${privateGb}`; memory.hidden = false; } else { memory.hidden = true; } stop.hidden = !(status?.owned && ["server_ready", "model_ready"].includes(status.state)); } async function refreshStatus(node, panel) { try { const response = await api.fetchApi(STATUS_ENDPOINT); if (!response.ok) throw new Error(`Status request failed (${response.status})`); const data = await response.json(); renderStatus(node, panel, data.sessions); } catch (_error) { renderStatus(node, panel, [], true); } } function inputIsSpeaker(input) { return /^speaker\d+$/.test(String(input?.name || "")); } function removeUnusedSpeakerInputs(node, maximum) { for (let index = (node.inputs || []).length - 1; index >= 0; index -= 1) { const input = node.inputs[index]; const number = Number(String(input?.name || "").replace("speaker", "")); if (inputIsSpeaker(input) && input.link == null && number > maximum) node.removeInput(index); } } function syncSpeakers(node, capability) { const native = capability?.native_multi_speaker || {}; const maximum = native.supported ? Number(native.max_speakers || 1) : 1; removeUnusedSpeakerInputs(node, maximum); const speakers = (node.inputs || []).filter(inputIsSpeaker); if (maximum <= 1) { for (let index = (node.inputs || []).length - 1; index >= 0; index -= 1) { if (inputIsSpeaker(node.inputs[index]) && node.inputs[index].link == null) node.removeInput(index); } return; } speakers.forEach((input, index) => { input.name = `speaker${index + 2}`; input.label = `Speaker ${index + 2}`; }); const last = speakers[speakers.length - 1]; if (speakers.length < maximum - 1 && (!last || last.link != null)) { node.addInput(`speaker${speakers.length + 2}`, "*"); } } function pill(text, tone = "normal", title = "") { const element = document.createElement("span"); element.textContent = text; element.className = `tts-acpp-pill ${tone}`; if (title) element.title = title; return element; } function appendAsrFeaturePills(container, capability) { const features = capability.asr_features || {}; if (features.diarization === "native") { container.append(pill( "Diarization", "special", "Native speaker-attributed turns are available from this ASR family.", )); } else { container.append(pill( "No diarization", "muted", "This ASR family does not return speaker identities.", )); } const timing = features.timing || "none"; if (timing === "native_word") { container.append(pill( "Word timestamps", "info", "Native word or token timestamps are available without a separate aligner.", )); } else if (timing === "native_segment") { container.append(pill( "Segment timestamps", "info", "Native timed transcript or speaker segments are available.", )); } else if (timing === "optional_forced_aligner") { container.append(pill( "Optional forced aligner", "warn", "Word timestamps require the separate Qwen3 Forced Aligner model.", )); } else { container.append(pill( "No timestamps", "muted", "This ASR family currently returns transcription text without timing alignment.", )); } } function setWidgetHeight(widget, height) { try { widget.height = height; } catch (_error) { /* getter-only on some builds */ } try { widget.computedHeight = height; } catch (_error) { /* getter-only on some builds */ } } function makePanel(node) { const panel = document.createElement("div"); panel.className = "tts-acpp-panel"; panel.innerHTML = `