From 0e11072b846dd6f29de717293ea698a35d5dda47 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 28 Mar 2026 08:52:29 +0000 Subject: [PATCH] fix: avoid speech runtime import in status output --- src/auto-reply/status.ts | 23 ++------ src/tts/status-config.test.ts | 54 ++++++++++++++++++ src/tts/status-config.ts | 100 ++++++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 18 deletions(-) create mode 100644 src/tts/status-config.test.ts create mode 100644 src/tts/status-config.ts diff --git a/src/auto-reply/status.ts b/src/auto-reply/status.ts index 2047da03c8d..a8290841325 100644 --- a/src/auto-reply/status.ts +++ b/src/auto-reply/status.ts @@ -28,14 +28,7 @@ import { resolveCommitHash } from "../infra/git-commit.js"; import type { MediaUnderstandingDecision } from "../media-understanding/types.js"; import { listPluginCommands } from "../plugins/commands.js"; import { resolveAgentIdFromSessionKey } from "../routing/session-key.js"; -import { - getTtsMaxLength, - getTtsProvider, - isSummarizationEnabled, - resolveTtsAutoMode, - resolveTtsConfig, - resolveTtsPrefsPath, -} from "../tts/tts.js"; +import { resolveStatusTtsSnapshot } from "../tts/status-config.js"; import { estimateUsageCost, formatTokenCount as formatTokenCountShared, @@ -398,20 +391,14 @@ const formatVoiceModeLine = ( if (!config) { return null; } - const ttsConfig = resolveTtsConfig(config); - const prefsPath = resolveTtsPrefsPath(ttsConfig); - const autoMode = resolveTtsAutoMode({ - config: ttsConfig, - prefsPath, + const snapshot = resolveStatusTtsSnapshot({ + cfg: config, sessionAuto: sessionEntry?.ttsAuto, }); - if (autoMode === "off") { + if (!snapshot) { return null; } - const provider = getTtsProvider(ttsConfig, prefsPath); - const maxLength = getTtsMaxLength(prefsPath); - const summarize = isSummarizationEnabled(prefsPath) ? "on" : "off"; - return `馃攰 Voice: ${autoMode} 路 provider=${provider} 路 limit=${maxLength} 路 summary=${summarize}`; + return `馃攰 Voice: ${snapshot.autoMode} 路 provider=${snapshot.provider} 路 limit=${snapshot.maxLength} 路 summary=${snapshot.summarize ? "on" : "off"}`; }; export function buildStatusMessage(args: StatusArgs): string { diff --git a/src/tts/status-config.test.ts b/src/tts/status-config.test.ts new file mode 100644 index 00000000000..7f1f40a070f --- /dev/null +++ b/src/tts/status-config.test.ts @@ -0,0 +1,54 @@ +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { withTempHome } from "../../test/helpers/temp-home.js"; +import type { OpenClawConfig } from "../config/config.js"; +import { resolveStatusTtsSnapshot } from "./status-config.js"; + +describe("resolveStatusTtsSnapshot", () => { + it("uses prefs overrides without loading speech providers", async () => { + await withTempHome(async (home) => { + const prefsPath = path.join(home, ".openclaw", "settings", "tts.json"); + fs.mkdirSync(path.dirname(prefsPath), { recursive: true }); + fs.writeFileSync( + prefsPath, + JSON.stringify({ + tts: { + auto: "always", + provider: "edge", + maxLength: 2048, + summarize: false, + }, + }), + ); + + expect(resolveStatusTtsSnapshot({ cfg: {} as OpenClawConfig })).toEqual({ + autoMode: "always", + provider: "microsoft", + maxLength: 2048, + summarize: false, + }); + }); + }); + + it("reports auto provider when tts is on without an explicit provider", async () => { + await withTempHome(async () => { + expect( + resolveStatusTtsSnapshot({ + cfg: { + messages: { + tts: { + auto: "always", + }, + }, + } as OpenClawConfig, + }), + ).toEqual({ + autoMode: "always", + provider: "auto", + maxLength: 1500, + summarize: true, + }); + }); + }); +}); diff --git a/src/tts/status-config.ts b/src/tts/status-config.ts new file mode 100644 index 00000000000..eeef87f9cfc --- /dev/null +++ b/src/tts/status-config.ts @@ -0,0 +1,100 @@ +import fs from "node:fs"; +import path from "node:path"; +import type { OpenClawConfig } from "../config/config.js"; +import type { TtsAutoMode, TtsConfig, TtsProvider } from "../config/types.tts.js"; +import { CONFIG_DIR, resolveUserPath } from "../utils.js"; +import { normalizeTtsAutoMode } from "./tts-auto-mode.js"; + +const DEFAULT_TTS_MAX_LENGTH = 1500; +const DEFAULT_TTS_SUMMARIZE = true; + +type TtsUserPrefs = { + tts?: { + auto?: TtsAutoMode; + enabled?: boolean; + provider?: TtsProvider; + maxLength?: number; + summarize?: boolean; + }; +}; + +type TtsStatusSnapshot = { + autoMode: TtsAutoMode; + provider: TtsProvider; + maxLength: number; + summarize: boolean; +}; + +function resolveConfiguredTtsAutoMode(raw: TtsConfig): TtsAutoMode { + return normalizeTtsAutoMode(raw.auto) ?? (raw.enabled ? "always" : "off"); +} + +function normalizeConfiguredSpeechProviderId( + providerId: string | undefined, +): TtsProvider | undefined { + const normalized = providerId?.trim().toLowerCase(); + if (!normalized) { + return undefined; + } + return normalized === "edge" ? "microsoft" : normalized; +} + +function resolveTtsPrefsPathValue(prefsPath: string | undefined): string { + if (prefsPath?.trim()) { + return resolveUserPath(prefsPath.trim()); + } + const envPath = process.env.OPENCLAW_TTS_PREFS?.trim(); + if (envPath) { + return resolveUserPath(envPath); + } + return path.join(CONFIG_DIR, "settings", "tts.json"); +} + +function readPrefs(prefsPath: string): TtsUserPrefs { + try { + if (!fs.existsSync(prefsPath)) { + return {}; + } + return JSON.parse(fs.readFileSync(prefsPath, "utf8")) as TtsUserPrefs; + } catch { + return {}; + } +} + +function resolveTtsAutoModeFromPrefs(prefs: TtsUserPrefs): TtsAutoMode | undefined { + const auto = normalizeTtsAutoMode(prefs.tts?.auto); + if (auto) { + return auto; + } + if (typeof prefs.tts?.enabled === "boolean") { + return prefs.tts.enabled ? "always" : "off"; + } + return undefined; +} + +export function resolveStatusTtsSnapshot(params: { + cfg: OpenClawConfig; + sessionAuto?: string; +}): TtsStatusSnapshot | null { + const raw: TtsConfig = params.cfg.messages?.tts ?? {}; + const prefsPath = resolveTtsPrefsPathValue(raw.prefsPath); + const prefs = readPrefs(prefsPath); + const autoMode = + normalizeTtsAutoMode(params.sessionAuto) ?? + resolveTtsAutoModeFromPrefs(prefs) ?? + resolveConfiguredTtsAutoMode(raw); + + if (autoMode === "off") { + return null; + } + + return { + autoMode, + provider: + normalizeConfiguredSpeechProviderId(prefs.tts?.provider) ?? + normalizeConfiguredSpeechProviderId(raw.provider) ?? + "auto", + maxLength: prefs.tts?.maxLength ?? DEFAULT_TTS_MAX_LENGTH, + summarize: prefs.tts?.summarize ?? DEFAULT_TTS_SUMMARIZE, + }; +}