openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
245 lines (244 loc) • 10.2 kB
JavaScript
import { readFileSync, statSync, writeFileSync } from "node:fs";
import path from "node:path";
import { CHROMIUM_FULL_VERSION, TRUSTED_CLIENT_TOKEN, generateSecMsGecToken } from "node-edge-tts/dist/drm.js";
import { asBoolean, asFiniteNumber, asOptionalRecord, filterStringRecord, normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { mkdir } from "node:fs/promises";
//#region extensions/microsoft/tts.ts
function inferEdgeExtension(outputFormat) {
const normalized = normalizeLowercaseStringOrEmpty(outputFormat);
if (normalized.includes("webm")) return ".webm";
if (normalized.includes("ogg")) return ".ogg";
if (normalized.includes("opus")) return ".opus";
if (normalized.includes("wav") || normalized.includes("riff") || normalized.includes("pcm")) return ".wav";
return ".mp3";
}
async function edgeTTS(params) {
const { text, outputPath, config, timeoutMs } = params;
if (text.trim().length === 0) throw new Error("Microsoft TTS text cannot be empty");
const { writeExternalFileWithinRoot } = await import("openclaw/plugin-sdk/security-runtime");
const tts = new (await (import("node-edge-tts"))).EdgeTTS({
voice: config.voice,
lang: config.lang,
outputFormat: config.outputFormat,
saveSubtitles: config.saveSubtitles,
proxy: config.proxy,
rate: config.rate,
pitch: config.pitch,
volume: config.volume,
timeout: config.timeoutMs ?? timeoutMs
});
await mkdir(path.dirname(outputPath), { recursive: true });
for (let attempt = 0; attempt < 2; attempt += 1) {
let outputSize = 0;
await writeExternalFileWithinRoot({
rootDir: path.dirname(outputPath),
path: path.basename(outputPath),
write: async (tempPath) => {
writeFileSync(tempPath, "");
await tts.ttsPromise(text, tempPath);
outputSize = statSync(tempPath).size;
}
});
if (outputSize > 0) return;
}
throw new Error("Edge TTS produced empty audio file after retry");
}
//#endregion
//#region extensions/microsoft/speech-provider.ts
const DEFAULT_EDGE_VOICE = "en-US-MichelleNeural";
const DEFAULT_EDGE_LANG = "en-US";
const DEFAULT_EDGE_OUTPUT_FORMAT = "audio-24khz-48kbitrate-mono-mp3";
const DEFAULT_MICROSOFT_VOICE_LIST_TIMEOUT_MS = 3e4;
function normalizeMicrosoftProviderConfig(rawConfig) {
const providers = asOptionalRecord(rawConfig.providers);
const rawEdge = asOptionalRecord(rawConfig.edge);
const rawMicrosoft = asOptionalRecord(rawConfig.microsoft);
const rawProviderMicrosoft = asOptionalRecord(providers?.microsoft);
const raw = {
...rawEdge,
...rawMicrosoft,
...rawProviderMicrosoft
};
return readMicrosoftProviderConfig({
...raw,
outputFormatConfigured: Boolean(normalizeOptionalString(raw.outputFormat))
});
}
function readMicrosoftProviderConfig(config) {
return {
enabled: asBoolean(config.enabled) ?? true,
voice: normalizeOptionalString(config.voice) ?? DEFAULT_EDGE_VOICE,
lang: normalizeOptionalString(config.lang) ?? DEFAULT_EDGE_LANG,
outputFormat: normalizeOptionalString(config.outputFormat) ?? DEFAULT_EDGE_OUTPUT_FORMAT,
outputFormatConfigured: asBoolean(config.outputFormatConfigured) ?? false,
pitch: normalizeOptionalString(config.pitch),
rate: normalizeOptionalString(config.rate),
volume: normalizeOptionalString(config.volume),
saveSubtitles: asBoolean(config.saveSubtitles) ?? false,
proxy: normalizeOptionalString(config.proxy),
timeoutMs: asFiniteNumber(config.timeoutMs)
};
}
function buildMicrosoftVoiceHeaders() {
const major = CHROMIUM_FULL_VERSION.split(".")[0] || "0";
return {
Authority: "speech.platform.bing.com",
Origin: "chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold",
Accept: "*/*",
"User-Agent": `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${major}.0.0.0 Safari/537.36 Edg/${major}.0.0.0`,
"Sec-MS-GEC": generateSecMsGecToken(),
"Sec-MS-GEC-Version": `1-${CHROMIUM_FULL_VERSION}`
};
}
function readMicrosoftVoiceTagStrings(value) {
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : void 0;
}
function isCjkDominant(text) {
const stripped = text.replace(/\s+/g, "");
if (stripped.length === 0) return false;
let cjkCount = 0;
for (const ch of stripped) {
const code = ch.codePointAt(0) ?? 0;
if (code >= 19968 && code <= 40959 || code >= 13312 && code <= 19903 || code >= 12288 && code <= 12351 || code >= 65280 && code <= 65519) cjkCount += 1;
}
return cjkCount / stripped.length > .3;
}
const DEFAULT_CHINESE_EDGE_VOICE = "zh-CN-XiaoxiaoNeural";
const DEFAULT_CHINESE_EDGE_LANG = "zh-CN";
async function listMicrosoftVoices(timeoutMs = DEFAULT_MICROSOFT_VOICE_LIST_TIMEOUT_MS) {
const { assertOkOrThrowProviderError, readProviderJsonResponse } = await import("openclaw/plugin-sdk/provider-http");
const proxyCaptureSdk = await import("openclaw/plugin-sdk/proxy-capture");
const captureHost = proxyCaptureSdk;
const { fetchWithSsrFGuard, ssrfPolicyFromHttpBaseUrlAllowedHostname } = await import("openclaw/plugin-sdk/ssrf-runtime");
const url = `https://speech.platform.bing.com/consumer/speech/synthesize/readaloud/voices/list?trustedclienttoken=${TRUSTED_CLIENT_TOKEN}`;
const headers = buildMicrosoftVoiceHeaders();
const { response, release } = await fetchWithSsrFGuard({
url,
init: { headers },
policy: ssrfPolicyFromHttpBaseUrlAllowedHostname("https://speech.platform.bing.com"),
auditContext: "microsoft.speech.voices",
timeoutMs
});
try {
if (!proxyCaptureSdk.isDebugProxyGlobalFetchPatchInstalled()) captureHost.captureHttpExchangeAsync?.({
url,
method: "GET",
requestHeaders: headers,
response,
transport: "http",
meta: {
provider: "microsoft",
capability: "speech-voices"
}
}).catch(() => {});
await assertOkOrThrowProviderError(response, "Microsoft voices API error");
const voices = await readProviderJsonResponse(response, "microsoft.speech-voices");
return Array.isArray(voices) ? voices.flatMap((value) => {
const voice = asOptionalRecord(value);
const id = normalizeOptionalString(voice?.ShortName);
if (!voice || !id) return [];
const voiceTag = asOptionalRecord(voice.VoiceTag);
const categories = readMicrosoftVoiceTagStrings(voiceTag?.ContentCategories);
const personalities = readMicrosoftVoiceTagStrings(voiceTag?.VoicePersonalities);
return [{
id,
name: normalizeOptionalString(voice.FriendlyName) ?? id,
category: categories?.[0],
description: personalities?.length ? personalities.join(", ") : void 0,
locale: normalizeOptionalString(voice.Locale),
gender: normalizeOptionalString(voice.Gender),
personalities
}];
}) : [];
} finally {
await release();
}
}
function buildMicrosoftSpeechProvider() {
return {
id: "microsoft",
label: "Microsoft",
aliases: ["edge"],
autoSelectOrder: 30,
resolveConfig: ({ rawConfig }) => normalizeMicrosoftProviderConfig(rawConfig),
resolveTalkConfig: ({ baseTtsConfig, talkProviderConfig }) => {
return {
...normalizeMicrosoftProviderConfig(baseTtsConfig),
enabled: true,
...filterStringRecord({
voice: normalizeOptionalString(talkProviderConfig.voiceId),
lang: normalizeOptionalString(talkProviderConfig.languageCode),
outputFormat: normalizeOptionalString(talkProviderConfig.outputFormat),
pitch: normalizeOptionalString(talkProviderConfig.pitch),
rate: normalizeOptionalString(talkProviderConfig.rate),
volume: normalizeOptionalString(talkProviderConfig.volume),
proxy: normalizeOptionalString(talkProviderConfig.proxy)
}),
...asFiniteNumber(talkProviderConfig.timeoutMs) == null ? {} : { timeoutMs: asFiniteNumber(talkProviderConfig.timeoutMs) }
};
},
resolveTalkOverrides: ({ params }) => filterStringRecord({
voice: normalizeOptionalString(params.voiceId),
outputFormat: normalizeOptionalString(params.outputFormat)
}) ?? {},
listVoices: async (req) => {
return await listMicrosoftVoices(readMicrosoftProviderConfig(req.providerConfig ?? {}).timeoutMs ?? req.timeoutMs);
},
isConfigured: ({ providerConfig }) => readMicrosoftProviderConfig(providerConfig).enabled,
synthesize: async (req) => {
const config = readMicrosoftProviderConfig(req.providerConfig);
const { isVoiceMessageCompatibleAudio } = await import("openclaw/plugin-sdk/media-runtime");
const { tempWorkspace, resolvePreferredOpenClawTmpDir } = await import("openclaw/plugin-sdk/temp-path");
const temp = await tempWorkspace({
rootDir: resolvePreferredOpenClawTmpDir(),
prefix: "tts-microsoft-"
});
const tempDir = temp.dir;
const overrideVoice = normalizeOptionalString(req.providerOverrides?.voice);
let voice = overrideVoice ?? config.voice;
let lang = config.lang;
const outputFormat = normalizeOptionalString(req.providerOverrides?.outputFormat) ?? config.outputFormat;
const fallbackOutputFormat = outputFormat !== DEFAULT_EDGE_OUTPUT_FORMAT ? DEFAULT_EDGE_OUTPUT_FORMAT : void 0;
if (!overrideVoice && voice === DEFAULT_EDGE_VOICE && isCjkDominant(req.text)) {
voice = DEFAULT_CHINESE_EDGE_VOICE;
lang = DEFAULT_CHINESE_EDGE_LANG;
}
try {
const runEdge = async (format) => {
const fileExtension = inferEdgeExtension(format);
const outputPath = path.join(tempDir, `speech${fileExtension}`);
await edgeTTS({
text: req.text,
outputPath,
config: {
...config,
voice,
lang,
outputFormat: format
},
timeoutMs: req.timeoutMs
});
return {
audioBuffer: readFileSync(outputPath),
outputFormat: format,
fileExtension,
voiceCompatible: isVoiceMessageCompatibleAudio({ fileName: outputPath })
};
};
try {
return await runEdge(outputFormat);
} catch (error) {
if (!fallbackOutputFormat) throw error;
return await runEdge(fallbackOutputFormat);
}
} finally {
await temp.cleanup();
}
}
};
}
//#endregion
//#region extensions/microsoft/capability-catalog.ts
var capability_catalog_default = { speechProviders: [buildMicrosoftSpeechProvider()] };
//#endregion
export { capability_catalog_default as default };