fix: avoid speech runtime import in status output

This commit is contained in:
Peter Steinberger 2026-03-28 08:52:29 +00:00
parent 85b3c1db30
commit 0e11072b84
3 changed files with 159 additions and 18 deletions

View File

@ -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 {

View File

@ -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,
});
});
});
});

100
src/tts/status-config.ts Normal file
View File

@ -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,
};
}