mirror of https://github.com/openclaw/openclaw.git
TTS: extract status surface
This commit is contained in:
parent
2c32b8834d
commit
d047f604d3
|
|
@ -1,14 +1,12 @@
|
|||
import {
|
||||
isExtensionHostTtsProviderConfigured,
|
||||
resolveExtensionHostTtsApiKey,
|
||||
} from "../../extension-host/tts-runtime-registry.js";
|
||||
formatExtensionHostTtsStatusText,
|
||||
resolveExtensionHostTtsStatusSnapshot,
|
||||
} from "../../extension-host/tts-status.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import {
|
||||
getLastTtsAttempt,
|
||||
getTtsMaxLength,
|
||||
getTtsProvider,
|
||||
isSummarizationEnabled,
|
||||
isTtsEnabled,
|
||||
resolveTtsConfig,
|
||||
resolveTtsPrefsPath,
|
||||
setLastTtsAttempt,
|
||||
|
|
@ -161,18 +159,16 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand
|
|||
if (action === "provider") {
|
||||
const currentProvider = getTtsProvider(config, prefsPath);
|
||||
if (!args.trim()) {
|
||||
const hasOpenAI = Boolean(resolveExtensionHostTtsApiKey(config, "openai"));
|
||||
const hasElevenLabs = Boolean(resolveExtensionHostTtsApiKey(config, "elevenlabs"));
|
||||
const hasEdge = isExtensionHostTtsProviderConfigured(config, "edge");
|
||||
const status = resolveExtensionHostTtsStatusSnapshot({ config, prefsPath });
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
text:
|
||||
`🎙️ TTS provider\n` +
|
||||
`Primary: ${currentProvider}\n` +
|
||||
`OpenAI key: ${hasOpenAI ? "✅" : "❌"}\n` +
|
||||
`ElevenLabs key: ${hasElevenLabs ? "✅" : "❌"}\n` +
|
||||
`Edge enabled: ${hasEdge ? "✅" : "❌"}\n` +
|
||||
`OpenAI key: ${status.hasOpenAIKey ? "✅" : "❌"}\n` +
|
||||
`ElevenLabs key: ${status.hasElevenLabsKey ? "✅" : "❌"}\n` +
|
||||
`Edge enabled: ${status.edgeEnabled ? "✅" : "❌"}\n` +
|
||||
`Usage: /tts provider openai | elevenlabs | edge`,
|
||||
},
|
||||
};
|
||||
|
|
@ -249,32 +245,8 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand
|
|||
}
|
||||
|
||||
if (action === "status") {
|
||||
const enabled = isTtsEnabled(config, prefsPath);
|
||||
const provider = getTtsProvider(config, prefsPath);
|
||||
const hasKey = isExtensionHostTtsProviderConfigured(config, provider);
|
||||
const maxLength = getTtsMaxLength(prefsPath);
|
||||
const summarize = isSummarizationEnabled(prefsPath);
|
||||
const last = getLastTtsAttempt();
|
||||
const lines = [
|
||||
"📊 TTS status",
|
||||
`State: ${enabled ? "✅ enabled" : "❌ disabled"}`,
|
||||
`Provider: ${provider} (${hasKey ? "✅ configured" : "❌ not configured"})`,
|
||||
`Text limit: ${maxLength} chars`,
|
||||
`Auto-summary: ${summarize ? "on" : "off"}`,
|
||||
];
|
||||
if (last) {
|
||||
const timeAgo = Math.round((Date.now() - last.timestamp) / 1000);
|
||||
lines.push("");
|
||||
lines.push(`Last attempt (${timeAgo}s ago): ${last.success ? "✅" : "❌"}`);
|
||||
lines.push(`Text: ${last.textLength} chars${last.summarized ? " (summarized)" : ""}`);
|
||||
if (last.success) {
|
||||
lines.push(`Provider: ${last.provider ?? "unknown"}`);
|
||||
lines.push(`Latency: ${last.latencyMs ?? 0}ms`);
|
||||
} else if (last.error) {
|
||||
lines.push(`Error: ${last.error}`);
|
||||
}
|
||||
}
|
||||
return { shouldContinue: false, reply: { text: lines.join("\n") } };
|
||||
const status = resolveExtensionHostTtsStatusSnapshot({ config, prefsPath });
|
||||
return { shouldContinue: false, reply: { text: formatExtensionHostTtsStatusText(status) } };
|
||||
}
|
||||
|
||||
return { shouldContinue: false, reply: ttsUsage() };
|
||||
|
|
|
|||
|
|
@ -0,0 +1,117 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
formatExtensionHostTtsStatusText,
|
||||
resolveExtensionHostTtsStatusSnapshot,
|
||||
setExtensionHostLastTtsAttempt,
|
||||
} from "./tts-status.js";
|
||||
|
||||
describe("tts-status", () => {
|
||||
it("builds a status snapshot from host-owned preferences and runtime state", () => {
|
||||
const config = {
|
||||
auto: "always",
|
||||
provider: "openai",
|
||||
providerSource: "config",
|
||||
prefsPath: "/tmp/tts-status.json",
|
||||
modelOverrides: {
|
||||
enabled: true,
|
||||
allowText: true,
|
||||
allowProvider: false,
|
||||
allowVoice: true,
|
||||
allowModelId: true,
|
||||
allowVoiceSettings: true,
|
||||
allowNormalization: true,
|
||||
allowSeed: true,
|
||||
},
|
||||
elevenlabs: {
|
||||
apiKey: undefined,
|
||||
baseUrl: "https://api.elevenlabs.io",
|
||||
voiceId: "voice-id",
|
||||
modelId: "eleven_multilingual_v2",
|
||||
voiceSettings: {
|
||||
stability: 0.5,
|
||||
similarityBoost: 0.75,
|
||||
style: 0,
|
||||
useSpeakerBoost: true,
|
||||
speed: 1,
|
||||
},
|
||||
},
|
||||
openai: {
|
||||
apiKey: "openai-key",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
model: "gpt-4o-mini-tts",
|
||||
voice: "alloy",
|
||||
},
|
||||
edge: {
|
||||
enabled: true,
|
||||
voice: "en-US-MichelleNeural",
|
||||
lang: "en-US",
|
||||
outputFormat: "audio-24khz-48kbitrate-mono-mp3",
|
||||
outputFormatConfigured: false,
|
||||
saveSubtitles: false,
|
||||
},
|
||||
mode: "final",
|
||||
maxTextLength: 4096,
|
||||
timeoutMs: 30000,
|
||||
};
|
||||
|
||||
const status = resolveExtensionHostTtsStatusSnapshot({
|
||||
config,
|
||||
prefsPath: "/tmp/tts-status.json",
|
||||
});
|
||||
|
||||
expect(status).toMatchObject({
|
||||
enabled: true,
|
||||
auto: "always",
|
||||
provider: "openai",
|
||||
providerConfigured: true,
|
||||
hasOpenAIKey: true,
|
||||
edgeEnabled: true,
|
||||
maxLength: 1500,
|
||||
summarize: true,
|
||||
});
|
||||
expect(status.fallbackProviders.length).toBeGreaterThan(0);
|
||||
expect(status.fallbackProviders).toContain(status.fallbackProvider);
|
||||
});
|
||||
|
||||
it("formats the last attempt details in the host-owned status text", () => {
|
||||
setExtensionHostLastTtsAttempt({
|
||||
timestamp: 1000,
|
||||
success: false,
|
||||
textLength: 42,
|
||||
summarized: true,
|
||||
error: "provider failed",
|
||||
});
|
||||
|
||||
const text = formatExtensionHostTtsStatusText(
|
||||
{
|
||||
enabled: true,
|
||||
auto: "always",
|
||||
provider: "openai",
|
||||
providerConfigured: true,
|
||||
fallbackProvider: "edge",
|
||||
fallbackProviders: ["edge"],
|
||||
prefsPath: "/tmp/tts-status.json",
|
||||
maxLength: 1500,
|
||||
summarize: true,
|
||||
hasOpenAIKey: true,
|
||||
hasElevenLabsKey: false,
|
||||
edgeEnabled: true,
|
||||
lastAttempt: {
|
||||
timestamp: 1000,
|
||||
success: false,
|
||||
textLength: 42,
|
||||
summarized: true,
|
||||
error: "provider failed",
|
||||
},
|
||||
},
|
||||
6000,
|
||||
);
|
||||
|
||||
expect(text).toContain("📊 TTS status");
|
||||
expect(text).toContain("Last attempt (5s ago): ❌");
|
||||
expect(text).toContain("Text: 42 chars (summarized)");
|
||||
expect(text).toContain("Error: provider failed");
|
||||
|
||||
setExtensionHostLastTtsAttempt(undefined);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
import type { TtsProvider } from "../config/types.tts.js";
|
||||
import type { ResolvedTtsConfig } from "../tts/tts.js";
|
||||
import {
|
||||
getExtensionHostTtsMaxLength,
|
||||
isExtensionHostTtsEnabled,
|
||||
isExtensionHostTtsSummarizationEnabled,
|
||||
resolveExtensionHostTtsAutoMode,
|
||||
} from "./tts-preferences.js";
|
||||
import {
|
||||
isExtensionHostTtsProviderConfigured,
|
||||
resolveExtensionHostTtsApiKey,
|
||||
resolveExtensionHostTtsProviderOrder,
|
||||
} from "./tts-runtime-registry.js";
|
||||
import { resolveExtensionHostTtsProvider } from "./tts-runtime-setup.js";
|
||||
|
||||
export type ExtensionHostTtsStatusEntry = {
|
||||
timestamp: number;
|
||||
success: boolean;
|
||||
textLength: number;
|
||||
summarized: boolean;
|
||||
provider?: string;
|
||||
latencyMs?: number;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export type ExtensionHostTtsStatusSnapshot = {
|
||||
enabled: boolean;
|
||||
auto: ReturnType<typeof resolveExtensionHostTtsAutoMode>;
|
||||
provider: TtsProvider;
|
||||
providerConfigured: boolean;
|
||||
fallbackProvider: TtsProvider | null;
|
||||
fallbackProviders: TtsProvider[];
|
||||
prefsPath: string;
|
||||
maxLength: number;
|
||||
summarize: boolean;
|
||||
hasOpenAIKey: boolean;
|
||||
hasElevenLabsKey: boolean;
|
||||
edgeEnabled: boolean;
|
||||
lastAttempt?: ExtensionHostTtsStatusEntry;
|
||||
};
|
||||
|
||||
let lastExtensionHostTtsAttempt: ExtensionHostTtsStatusEntry | undefined;
|
||||
|
||||
export function getExtensionHostLastTtsAttempt(): ExtensionHostTtsStatusEntry | undefined {
|
||||
return lastExtensionHostTtsAttempt;
|
||||
}
|
||||
|
||||
export function setExtensionHostLastTtsAttempt(
|
||||
entry: ExtensionHostTtsStatusEntry | undefined,
|
||||
): void {
|
||||
lastExtensionHostTtsAttempt = entry;
|
||||
}
|
||||
|
||||
export function resolveExtensionHostTtsStatusSnapshot(params: {
|
||||
config: ResolvedTtsConfig;
|
||||
prefsPath: string;
|
||||
}): ExtensionHostTtsStatusSnapshot {
|
||||
const { config, prefsPath } = params;
|
||||
const provider = resolveExtensionHostTtsProvider(config, prefsPath);
|
||||
const fallbackProviders = resolveExtensionHostTtsProviderOrder(provider)
|
||||
.slice(1)
|
||||
.filter((candidate) => isExtensionHostTtsProviderConfigured(config, candidate));
|
||||
return {
|
||||
enabled: isExtensionHostTtsEnabled(config, prefsPath),
|
||||
auto: resolveExtensionHostTtsAutoMode({ config, prefsPath }),
|
||||
provider,
|
||||
providerConfigured: isExtensionHostTtsProviderConfigured(config, provider),
|
||||
fallbackProvider: fallbackProviders[0] ?? null,
|
||||
fallbackProviders,
|
||||
prefsPath,
|
||||
maxLength: getExtensionHostTtsMaxLength(prefsPath),
|
||||
summarize: isExtensionHostTtsSummarizationEnabled(prefsPath),
|
||||
hasOpenAIKey: Boolean(resolveExtensionHostTtsApiKey(config, "openai")),
|
||||
hasElevenLabsKey: Boolean(resolveExtensionHostTtsApiKey(config, "elevenlabs")),
|
||||
edgeEnabled: isExtensionHostTtsProviderConfigured(config, "edge"),
|
||||
lastAttempt: getExtensionHostLastTtsAttempt(),
|
||||
};
|
||||
}
|
||||
|
||||
export function formatExtensionHostTtsStatusText(
|
||||
status: ExtensionHostTtsStatusSnapshot,
|
||||
now = Date.now(),
|
||||
): string {
|
||||
const lines = [
|
||||
"📊 TTS status",
|
||||
`State: ${status.enabled ? "✅ enabled" : "❌ disabled"}`,
|
||||
`Provider: ${status.provider} (${status.providerConfigured ? "✅ configured" : "❌ not configured"})`,
|
||||
`Text limit: ${status.maxLength} chars`,
|
||||
`Auto-summary: ${status.summarize ? "on" : "off"}`,
|
||||
];
|
||||
if (!status.lastAttempt) {
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
const timeAgo = Math.round((now - status.lastAttempt.timestamp) / 1000);
|
||||
lines.push("");
|
||||
lines.push(`Last attempt (${timeAgo}s ago): ${status.lastAttempt.success ? "✅" : "❌"}`);
|
||||
lines.push(
|
||||
`Text: ${status.lastAttempt.textLength} chars${status.lastAttempt.summarized ? " (summarized)" : ""}`,
|
||||
);
|
||||
if (status.lastAttempt.success) {
|
||||
lines.push(`Provider: ${status.lastAttempt.provider ?? "unknown"}`);
|
||||
lines.push(`Latency: ${status.lastAttempt.latencyMs ?? 0}ms`);
|
||||
} else if (status.lastAttempt.error) {
|
||||
lines.push(`Error: ${status.lastAttempt.error}`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
|
@ -1,15 +1,10 @@
|
|||
import { loadConfig } from "../../config/config.js";
|
||||
import {
|
||||
isExtensionHostTtsProviderConfigured,
|
||||
resolveExtensionHostTtsApiKey,
|
||||
resolveExtensionHostTtsProviderOrder,
|
||||
} from "../../extension-host/tts-runtime-registry.js";
|
||||
import { isExtensionHostTtsProviderConfigured } from "../../extension-host/tts-runtime-registry.js";
|
||||
import { resolveExtensionHostTtsStatusSnapshot } from "../../extension-host/tts-status.js";
|
||||
import {
|
||||
OPENAI_TTS_MODELS,
|
||||
OPENAI_TTS_VOICES,
|
||||
getTtsProvider,
|
||||
isTtsEnabled,
|
||||
resolveTtsAutoMode,
|
||||
resolveTtsConfig,
|
||||
resolveTtsPrefsPath,
|
||||
setTtsEnabled,
|
||||
|
|
@ -26,22 +21,7 @@ export const ttsHandlers: GatewayRequestHandlers = {
|
|||
const cfg = loadConfig();
|
||||
const config = resolveTtsConfig(cfg);
|
||||
const prefsPath = resolveTtsPrefsPath(config);
|
||||
const provider = getTtsProvider(config, prefsPath);
|
||||
const autoMode = resolveTtsAutoMode({ config, prefsPath });
|
||||
const fallbackProviders = resolveExtensionHostTtsProviderOrder(provider)
|
||||
.slice(1)
|
||||
.filter((candidate) => isExtensionHostTtsProviderConfigured(config, candidate));
|
||||
respond(true, {
|
||||
enabled: isTtsEnabled(config, prefsPath),
|
||||
auto: autoMode,
|
||||
provider,
|
||||
fallbackProvider: fallbackProviders[0] ?? null,
|
||||
fallbackProviders,
|
||||
prefsPath,
|
||||
hasOpenAIKey: Boolean(resolveExtensionHostTtsApiKey(config, "openai")),
|
||||
hasElevenLabsKey: Boolean(resolveExtensionHostTtsApiKey(config, "elevenlabs")),
|
||||
edgeEnabled: isExtensionHostTtsProviderConfigured(config, "edge"),
|
||||
});
|
||||
respond(true, resolveExtensionHostTtsStatusSnapshot({ config, prefsPath }));
|
||||
} catch (err) {
|
||||
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,11 @@ import {
|
|||
resolveExtensionHostTtsProvider,
|
||||
resolveExtensionHostTtsRequestSetup,
|
||||
} from "../extension-host/tts-runtime-setup.js";
|
||||
import {
|
||||
getExtensionHostLastTtsAttempt,
|
||||
setExtensionHostLastTtsAttempt,
|
||||
type ExtensionHostTtsStatusEntry,
|
||||
} from "../extension-host/tts-status.js";
|
||||
import { logVerbose } from "../globals.js";
|
||||
import {
|
||||
DEFAULT_OPENAI_BASE_URL,
|
||||
|
|
@ -178,17 +183,7 @@ export type TtsTelephonyResult = {
|
|||
sampleRate?: number;
|
||||
};
|
||||
|
||||
type TtsStatusEntry = {
|
||||
timestamp: number;
|
||||
success: boolean;
|
||||
textLength: number;
|
||||
summarized: boolean;
|
||||
provider?: string;
|
||||
latencyMs?: number;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
let lastTtsAttempt: TtsStatusEntry | undefined;
|
||||
type TtsStatusEntry = ExtensionHostTtsStatusEntry;
|
||||
|
||||
export const normalizeTtsAutoMode = normalizeExtensionHostTtsAutoMode;
|
||||
|
||||
|
|
@ -341,11 +336,11 @@ export const isSummarizationEnabled = isExtensionHostTtsSummarizationEnabled;
|
|||
export const setSummarizationEnabled = setExtensionHostTtsSummarizationEnabled;
|
||||
|
||||
export function getLastTtsAttempt(): TtsStatusEntry | undefined {
|
||||
return lastTtsAttempt;
|
||||
return getExtensionHostLastTtsAttempt();
|
||||
}
|
||||
|
||||
export function setLastTtsAttempt(entry: TtsStatusEntry | undefined): void {
|
||||
lastTtsAttempt = entry;
|
||||
setExtensionHostLastTtsAttempt(entry);
|
||||
}
|
||||
|
||||
export const TTS_PROVIDERS = EXTENSION_HOST_TTS_PROVIDER_IDS;
|
||||
|
|
@ -440,14 +435,14 @@ export async function maybeApplyTtsToPayload(params: {
|
|||
});
|
||||
|
||||
if (result.success && result.audioPath) {
|
||||
lastTtsAttempt = {
|
||||
setExtensionHostLastTtsAttempt({
|
||||
timestamp: Date.now(),
|
||||
success: true,
|
||||
textLength: (params.payload.text ?? "").length,
|
||||
summarized: plan.wasSummarized,
|
||||
provider: result.provider,
|
||||
latencyMs: result.latencyMs,
|
||||
};
|
||||
});
|
||||
|
||||
const shouldVoice =
|
||||
isExtensionHostTtsVoiceBubbleChannel(params.channel) && result.voiceCompatible === true;
|
||||
|
|
@ -459,13 +454,13 @@ export async function maybeApplyTtsToPayload(params: {
|
|||
return finalPayload;
|
||||
}
|
||||
|
||||
lastTtsAttempt = {
|
||||
setExtensionHostLastTtsAttempt({
|
||||
timestamp: Date.now(),
|
||||
success: false,
|
||||
textLength: (params.payload.text ?? "").length,
|
||||
summarized: plan.wasSummarized,
|
||||
error: result.error,
|
||||
};
|
||||
});
|
||||
|
||||
const latency = Date.now() - ttsStart;
|
||||
logVerbose(`TTS: conversion failed after ${latency}ms (${result.error ?? "unknown"}).`);
|
||||
|
|
|
|||
Loading…
Reference in New Issue