TTS: extract runtime registry
This commit is contained in:
parent
12ba14c10f
commit
6c92171a47
@ -1,3 +1,7 @@
|
||||
import {
|
||||
isExtensionHostTtsProviderConfigured,
|
||||
resolveExtensionHostTtsApiKey,
|
||||
} from "../../extension-host/tts-runtime-registry.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import {
|
||||
getLastTtsAttempt,
|
||||
@ -5,8 +9,6 @@ import {
|
||||
getTtsProvider,
|
||||
isSummarizationEnabled,
|
||||
isTtsEnabled,
|
||||
isTtsProviderConfigured,
|
||||
resolveTtsApiKey,
|
||||
resolveTtsConfig,
|
||||
resolveTtsPrefsPath,
|
||||
setLastTtsAttempt,
|
||||
@ -159,9 +161,9 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand
|
||||
if (action === "provider") {
|
||||
const currentProvider = getTtsProvider(config, prefsPath);
|
||||
if (!args.trim()) {
|
||||
const hasOpenAI = Boolean(resolveTtsApiKey(config, "openai"));
|
||||
const hasElevenLabs = Boolean(resolveTtsApiKey(config, "elevenlabs"));
|
||||
const hasEdge = isTtsProviderConfigured(config, "edge");
|
||||
const hasOpenAI = Boolean(resolveExtensionHostTtsApiKey(config, "openai"));
|
||||
const hasElevenLabs = Boolean(resolveExtensionHostTtsApiKey(config, "elevenlabs"));
|
||||
const hasEdge = isExtensionHostTtsProviderConfigured(config, "edge");
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
@ -249,7 +251,7 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand
|
||||
if (action === "status") {
|
||||
const enabled = isTtsEnabled(config, prefsPath);
|
||||
const provider = getTtsProvider(config, prefsPath);
|
||||
const hasKey = isTtsProviderConfigured(config, provider);
|
||||
const hasKey = isExtensionHostTtsProviderConfigured(config, provider);
|
||||
const maxLength = getTtsMaxLength(prefsPath);
|
||||
const summarize = isSummarizationEnabled(prefsPath);
|
||||
const last = getLastTtsAttempt();
|
||||
|
||||
52
src/extension-host/tts-runtime-registry.test.ts
Normal file
52
src/extension-host/tts-runtime-registry.test.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
EXTENSION_HOST_TTS_PROVIDER_IDS,
|
||||
isExtensionHostTtsProviderConfigured,
|
||||
resolveExtensionHostTtsApiKey,
|
||||
resolveExtensionHostTtsProviderOrder,
|
||||
supportsExtensionHostTtsTelephony,
|
||||
} from "./tts-runtime-registry.js";
|
||||
|
||||
describe("extension host TTS runtime registry", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("keeps the built-in provider order stable", () => {
|
||||
expect(EXTENSION_HOST_TTS_PROVIDER_IDS).toEqual(["openai", "elevenlabs", "edge"]);
|
||||
expect(resolveExtensionHostTtsProviderOrder("edge")).toEqual(["edge", "openai", "elevenlabs"]);
|
||||
});
|
||||
|
||||
it("resolves API keys for remote providers", () => {
|
||||
const config = {
|
||||
openai: { apiKey: "openai-key" },
|
||||
elevenlabs: { apiKey: "xi-key" },
|
||||
edge: { enabled: false },
|
||||
} as const;
|
||||
|
||||
expect(resolveExtensionHostTtsApiKey(config, "openai")).toBe("openai-key");
|
||||
expect(resolveExtensionHostTtsApiKey(config, "elevenlabs")).toBe("xi-key");
|
||||
expect(resolveExtensionHostTtsApiKey(config, "edge")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("checks provider configuration through the host-owned definitions", () => {
|
||||
vi.stubEnv("ELEVENLABS_API_KEY", "");
|
||||
vi.stubEnv("XI_API_KEY", "");
|
||||
|
||||
const config = {
|
||||
openai: { apiKey: "openai-key" },
|
||||
elevenlabs: { apiKey: "" },
|
||||
edge: { enabled: true },
|
||||
} as const;
|
||||
|
||||
expect(isExtensionHostTtsProviderConfigured(config, "openai")).toBe(true);
|
||||
expect(isExtensionHostTtsProviderConfigured(config, "elevenlabs")).toBe(false);
|
||||
expect(isExtensionHostTtsProviderConfigured(config, "edge")).toBe(true);
|
||||
});
|
||||
|
||||
it("tracks telephony support per provider", () => {
|
||||
expect(supportsExtensionHostTtsTelephony("openai")).toBe(true);
|
||||
expect(supportsExtensionHostTtsTelephony("elevenlabs")).toBe(true);
|
||||
expect(supportsExtensionHostTtsTelephony("edge")).toBe(false);
|
||||
});
|
||||
});
|
||||
78
src/extension-host/tts-runtime-registry.ts
Normal file
78
src/extension-host/tts-runtime-registry.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import type { TtsProvider } from "../config/types.tts.js";
|
||||
import type { ResolvedTtsConfig } from "../tts/tts.js";
|
||||
|
||||
export type ExtensionHostTtsRuntimeProvider = {
|
||||
id: TtsProvider;
|
||||
supportsTelephony: boolean;
|
||||
resolveApiKey: (config: ResolvedTtsConfig) => string | undefined;
|
||||
isConfigured: (config: ResolvedTtsConfig) => boolean;
|
||||
};
|
||||
|
||||
const EXTENSION_HOST_TTS_RUNTIME_PROVIDERS: readonly ExtensionHostTtsRuntimeProvider[] = [
|
||||
{
|
||||
id: "openai",
|
||||
supportsTelephony: true,
|
||||
resolveApiKey(config) {
|
||||
return config.openai.apiKey || process.env.OPENAI_API_KEY;
|
||||
},
|
||||
isConfigured(config) {
|
||||
return Boolean(this.resolveApiKey(config));
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "elevenlabs",
|
||||
supportsTelephony: true,
|
||||
resolveApiKey(config) {
|
||||
return config.elevenlabs.apiKey || process.env.ELEVENLABS_API_KEY || process.env.XI_API_KEY;
|
||||
},
|
||||
isConfigured(config) {
|
||||
return Boolean(this.resolveApiKey(config));
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "edge",
|
||||
supportsTelephony: false,
|
||||
resolveApiKey() {
|
||||
return undefined;
|
||||
},
|
||||
isConfigured(config) {
|
||||
return config.edge.enabled;
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const EXTENSION_HOST_TTS_PROVIDER_IDS = EXTENSION_HOST_TTS_RUNTIME_PROVIDERS.map(
|
||||
(provider) => provider.id,
|
||||
) as readonly TtsProvider[];
|
||||
|
||||
export function listExtensionHostTtsRuntimeProviders(): readonly ExtensionHostTtsRuntimeProvider[] {
|
||||
return EXTENSION_HOST_TTS_RUNTIME_PROVIDERS;
|
||||
}
|
||||
|
||||
export function getExtensionHostTtsRuntimeProvider(
|
||||
id: TtsProvider,
|
||||
): ExtensionHostTtsRuntimeProvider | undefined {
|
||||
return EXTENSION_HOST_TTS_RUNTIME_PROVIDERS.find((provider) => provider.id === id);
|
||||
}
|
||||
|
||||
export function resolveExtensionHostTtsApiKey(
|
||||
config: ResolvedTtsConfig,
|
||||
provider: TtsProvider,
|
||||
): string | undefined {
|
||||
return getExtensionHostTtsRuntimeProvider(provider)?.resolveApiKey(config);
|
||||
}
|
||||
|
||||
export function isExtensionHostTtsProviderConfigured(
|
||||
config: ResolvedTtsConfig,
|
||||
provider: TtsProvider,
|
||||
): boolean {
|
||||
return getExtensionHostTtsRuntimeProvider(provider)?.isConfigured(config) ?? false;
|
||||
}
|
||||
|
||||
export function resolveExtensionHostTtsProviderOrder(primary: TtsProvider): TtsProvider[] {
|
||||
return [primary, ...EXTENSION_HOST_TTS_PROVIDER_IDS.filter((provider) => provider !== primary)];
|
||||
}
|
||||
|
||||
export function supportsExtensionHostTtsTelephony(provider: TtsProvider): boolean {
|
||||
return getExtensionHostTtsRuntimeProvider(provider)?.supportsTelephony ?? false;
|
||||
}
|
||||
@ -1,15 +1,17 @@
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import {
|
||||
isExtensionHostTtsProviderConfigured,
|
||||
resolveExtensionHostTtsApiKey,
|
||||
resolveExtensionHostTtsProviderOrder,
|
||||
} from "../../extension-host/tts-runtime-registry.js";
|
||||
import {
|
||||
OPENAI_TTS_MODELS,
|
||||
OPENAI_TTS_VOICES,
|
||||
getTtsProvider,
|
||||
isTtsEnabled,
|
||||
isTtsProviderConfigured,
|
||||
resolveTtsAutoMode,
|
||||
resolveTtsApiKey,
|
||||
resolveTtsConfig,
|
||||
resolveTtsPrefsPath,
|
||||
resolveTtsProviderOrder,
|
||||
setTtsEnabled,
|
||||
setTtsProvider,
|
||||
textToSpeech,
|
||||
@ -26,9 +28,9 @@ export const ttsHandlers: GatewayRequestHandlers = {
|
||||
const prefsPath = resolveTtsPrefsPath(config);
|
||||
const provider = getTtsProvider(config, prefsPath);
|
||||
const autoMode = resolveTtsAutoMode({ config, prefsPath });
|
||||
const fallbackProviders = resolveTtsProviderOrder(provider)
|
||||
const fallbackProviders = resolveExtensionHostTtsProviderOrder(provider)
|
||||
.slice(1)
|
||||
.filter((candidate) => isTtsProviderConfigured(config, candidate));
|
||||
.filter((candidate) => isExtensionHostTtsProviderConfigured(config, candidate));
|
||||
respond(true, {
|
||||
enabled: isTtsEnabled(config, prefsPath),
|
||||
auto: autoMode,
|
||||
@ -36,9 +38,9 @@ export const ttsHandlers: GatewayRequestHandlers = {
|
||||
fallbackProvider: fallbackProviders[0] ?? null,
|
||||
fallbackProviders,
|
||||
prefsPath,
|
||||
hasOpenAIKey: Boolean(resolveTtsApiKey(config, "openai")),
|
||||
hasElevenLabsKey: Boolean(resolveTtsApiKey(config, "elevenlabs")),
|
||||
edgeEnabled: isTtsProviderConfigured(config, "edge"),
|
||||
hasOpenAIKey: Boolean(resolveExtensionHostTtsApiKey(config, "openai")),
|
||||
hasElevenLabsKey: Boolean(resolveExtensionHostTtsApiKey(config, "elevenlabs")),
|
||||
edgeEnabled: isExtensionHostTtsProviderConfigured(config, "edge"),
|
||||
});
|
||||
} catch (err) {
|
||||
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)));
|
||||
@ -131,20 +133,20 @@ export const ttsHandlers: GatewayRequestHandlers = {
|
||||
{
|
||||
id: "openai",
|
||||
name: "OpenAI",
|
||||
configured: Boolean(resolveTtsApiKey(config, "openai")),
|
||||
configured: Boolean(resolveExtensionHostTtsApiKey(config, "openai")),
|
||||
models: [...OPENAI_TTS_MODELS],
|
||||
voices: [...OPENAI_TTS_VOICES],
|
||||
},
|
||||
{
|
||||
id: "elevenlabs",
|
||||
name: "ElevenLabs",
|
||||
configured: Boolean(resolveTtsApiKey(config, "elevenlabs")),
|
||||
configured: Boolean(resolveExtensionHostTtsApiKey(config, "elevenlabs")),
|
||||
models: ["eleven_multilingual_v2", "eleven_turbo_v2_5", "eleven_monolingual_v1"],
|
||||
},
|
||||
{
|
||||
id: "edge",
|
||||
name: "Edge TTS",
|
||||
configured: isTtsProviderConfigured(config, "edge"),
|
||||
configured: isExtensionHostTtsProviderConfigured(config, "edge"),
|
||||
models: [],
|
||||
},
|
||||
],
|
||||
|
||||
@ -22,6 +22,13 @@ import type {
|
||||
TtsProvider,
|
||||
TtsModelOverrideConfig,
|
||||
} from "../config/types.tts.js";
|
||||
import {
|
||||
EXTENSION_HOST_TTS_PROVIDER_IDS,
|
||||
isExtensionHostTtsProviderConfigured,
|
||||
resolveExtensionHostTtsApiKey,
|
||||
resolveExtensionHostTtsProviderOrder,
|
||||
supportsExtensionHostTtsTelephony,
|
||||
} from "../extension-host/tts-runtime-registry.js";
|
||||
import { logVerbose } from "../globals.js";
|
||||
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
|
||||
import { stripMarkdown } from "../line/markdown-to-line.js";
|
||||
@ -518,31 +525,13 @@ function resolveEdgeOutputFormat(config: ResolvedTtsConfig): string {
|
||||
return config.edge.outputFormat;
|
||||
}
|
||||
|
||||
export function resolveTtsApiKey(
|
||||
config: ResolvedTtsConfig,
|
||||
provider: TtsProvider,
|
||||
): string | undefined {
|
||||
if (provider === "elevenlabs") {
|
||||
return config.elevenlabs.apiKey || process.env.ELEVENLABS_API_KEY || process.env.XI_API_KEY;
|
||||
}
|
||||
if (provider === "openai") {
|
||||
return config.openai.apiKey || process.env.OPENAI_API_KEY;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
export const TTS_PROVIDERS = EXTENSION_HOST_TTS_PROVIDER_IDS;
|
||||
|
||||
export const TTS_PROVIDERS = ["openai", "elevenlabs", "edge"] as const;
|
||||
export const resolveTtsApiKey = resolveExtensionHostTtsApiKey;
|
||||
|
||||
export function resolveTtsProviderOrder(primary: TtsProvider): TtsProvider[] {
|
||||
return [primary, ...TTS_PROVIDERS.filter((provider) => provider !== primary)];
|
||||
}
|
||||
export const resolveTtsProviderOrder = resolveExtensionHostTtsProviderOrder;
|
||||
|
||||
export function isTtsProviderConfigured(config: ResolvedTtsConfig, provider: TtsProvider): boolean {
|
||||
if (provider === "edge") {
|
||||
return config.edge.enabled;
|
||||
}
|
||||
return Boolean(resolveTtsApiKey(config, provider));
|
||||
}
|
||||
export const isTtsProviderConfigured = isExtensionHostTtsProviderConfigured;
|
||||
|
||||
function formatTtsProviderError(provider: TtsProvider, err: unknown): string {
|
||||
const error = err instanceof Error ? err : new Error(String(err));
|
||||
@ -584,7 +573,7 @@ function resolveTtsRequestSetup(params: {
|
||||
const provider = params.providerOverride ?? userProvider;
|
||||
return {
|
||||
config,
|
||||
providers: resolveTtsProviderOrder(provider),
|
||||
providers: resolveExtensionHostTtsProviderOrder(provider),
|
||||
};
|
||||
}
|
||||
|
||||
@ -684,7 +673,7 @@ export async function textToSpeech(params: {
|
||||
};
|
||||
}
|
||||
|
||||
const apiKey = resolveTtsApiKey(config, provider);
|
||||
const apiKey = resolveExtensionHostTtsApiKey(config, provider);
|
||||
if (!apiKey) {
|
||||
errors.push(`${provider}: no API key`);
|
||||
continue;
|
||||
@ -776,12 +765,12 @@ export async function textToSpeechTelephony(params: {
|
||||
for (const provider of providers) {
|
||||
const providerStart = Date.now();
|
||||
try {
|
||||
if (provider === "edge") {
|
||||
if (!supportsExtensionHostTtsTelephony(provider)) {
|
||||
errors.push("edge: unsupported for telephony");
|
||||
continue;
|
||||
}
|
||||
|
||||
const apiKey = resolveTtsApiKey(config, provider);
|
||||
const apiKey = resolveExtensionHostTtsApiKey(config, provider);
|
||||
if (!apiKey) {
|
||||
errors.push(`${provider}: no API key`);
|
||||
continue;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user