Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled

Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
2026-07-05 09:36:54 +00:00
parent 3216769225
commit bedb527145
21108 changed files with 6010766 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
// Microsoft plugin entrypoint registers its OpenClaw integration.
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { buildMicrosoftSpeechProvider } from "./speech-provider.js";
export default definePluginEntry({
id: "microsoft",
name: "Microsoft Speech",
description: "Bundled Microsoft speech provider",
register(api) {
api.registerSpeechProvider(buildMicrosoftSpeechProvider());
},
});

View File

@@ -0,0 +1,15 @@
// Microsoft tests cover microsoft plugin behavior.
import { isLiveTestEnabled } from "openclaw/plugin-sdk/test-env";
import { describe, expect, it } from "vitest";
import { listMicrosoftVoices } from "./speech-provider.js";
const describeLive = isLiveTestEnabled() ? describe : describe.skip;
describeLive("microsoft plugin live", () => {
it("lists Edge speech voices", async () => {
const voices = await listMicrosoftVoices();
expect(voices.length).toBeGreaterThan(100);
expect(voices.map((voice) => voice.id)).toContain("en-US-MichelleNeural");
}, 60_000);
});

View File

@@ -0,0 +1,15 @@
{
"id": "microsoft",
"activation": {
"onStartup": false
},
"enabledByDefault": true,
"contracts": {
"speechProviders": ["microsoft"]
},
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}

View File

@@ -0,0 +1,18 @@
{
"name": "@openclaw/microsoft-speech",
"version": "2026.6.11",
"private": true,
"description": "OpenClaw Microsoft speech plugin",
"type": "module",
"dependencies": {
"node-edge-tts": "1.2.10"
},
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {
"extensions": [
"./index.ts"
]
}
}

View File

@@ -0,0 +1,287 @@
// Microsoft tests cover speech provider plugin behavior.
import { mkdtempSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import {
finalizeDebugProxyCapture,
getDebugProxyCaptureStore,
initializeDebugProxyCapture,
} from "openclaw/plugin-sdk/proxy-capture";
import { afterEach, describe, expect, it, vi } from "vitest";
import { installDebugProxyTestResetHooks } from "../test-support/debug-proxy-env-test-helpers.js";
vi.mock("node-edge-tts", () => ({
EdgeTTS: class {
async ttsPromise(): Promise<void> {}
},
}));
import {
buildMicrosoftSpeechProvider,
isCjkDominant,
listMicrosoftVoices,
} from "./speech-provider.js";
import * as ttsModule from "./tts.js";
const TEST_CFG = {} as OpenClawConfig;
function requireFirstEdgeTtsCall(edgeSpy: ReturnType<typeof vi.spyOn>): {
config?: unknown;
outputPath: string;
text?: string;
timeoutMs?: number;
} {
const [call] = edgeSpy.mock.calls;
if (!call) {
throw new Error("expected Microsoft Edge TTS call");
}
const [edgeCall] = call;
if (!edgeCall || typeof edgeCall !== "object" || Array.isArray(edgeCall)) {
throw new Error("expected Microsoft Edge TTS call");
}
return edgeCall as {
config?: unknown;
outputPath: string;
text?: string;
timeoutMs?: number;
};
}
describe("listMicrosoftVoices", () => {
const proxyReset = installDebugProxyTestResetHooks();
it("maps Microsoft voice metadata into speech voice options", async () => {
globalThis.fetch = vi.fn().mockResolvedValue(
new Response(
JSON.stringify([
{
ShortName: "en-US-AvaNeural",
FriendlyName: "Microsoft Ava Online (Natural) - English (United States)",
Locale: "en-US",
Gender: "Female",
VoiceTag: {
ContentCategories: ["General"],
VoicePersonalities: ["Friendly", "Positive"],
},
},
]),
{ status: 200 },
),
) as unknown as typeof globalThis.fetch;
const voices = await listMicrosoftVoices();
expect(voices).toEqual([
{
id: "en-US-AvaNeural",
name: "Microsoft Ava Online (Natural) - English (United States)",
category: "General",
description: "Friendly, Positive",
locale: "en-US",
gender: "Female",
personalities: ["Friendly", "Positive"],
},
]);
});
it("throws on Microsoft voice list failures", async () => {
globalThis.fetch = vi
.fn()
.mockResolvedValue(
new Response("nope", { status: 503 }),
) as unknown as typeof globalThis.fetch;
await expect(listMicrosoftVoices()).rejects.toThrow("Microsoft voices API error (503)");
});
it("records voice discovery exchanges in debug proxy capture mode", async () => {
const tempDir = mkdtempSync(path.join(os.tmpdir(), "microsoft-voices-capture-"));
proxyReset.captureProxyEnv();
process.env.OPENCLAW_DEBUG_PROXY_ENABLED = "1";
process.env.OPENCLAW_STATE_DIR = tempDir;
process.env.OPENCLAW_DEBUG_PROXY_SESSION_ID = "ms-voices-session";
globalThis.fetch = vi
.fn()
.mockResolvedValue(
new Response(JSON.stringify([{ ShortName: "en-US-AvaNeural" }]), { status: 200 }),
) as unknown as typeof globalThis.fetch;
const store = getDebugProxyCaptureStore();
store.upsertSession({
id: "ms-voices-session",
startedAt: Date.now(),
mode: "test",
sourceScope: "openclaw",
sourceProcess: "openclaw",
});
await listMicrosoftVoices();
await vi.waitFor(() => {
const events = store.getSessionEvents("ms-voices-session", 10);
expect(
events.some(
(event) => event.kind === "request" && event.host === "speech.platform.bing.com",
),
).toBe(true);
expect(
events.some(
(event) => event.kind === "response" && event.host === "speech.platform.bing.com",
),
).toBe(true);
});
});
it("does not double-capture voice discovery when the global fetch patch is installed", async () => {
const tempDir = mkdtempSync(path.join(os.tmpdir(), "microsoft-voices-global-"));
proxyReset.captureProxyEnv();
process.env.OPENCLAW_DEBUG_PROXY_ENABLED = "1";
process.env.OPENCLAW_STATE_DIR = tempDir;
process.env.OPENCLAW_DEBUG_PROXY_SESSION_ID = "ms-voices-global-session";
globalThis.fetch = vi.fn(
async () => new Response(JSON.stringify([{ ShortName: "en-US-AvaNeural" }]), { status: 200 }),
) as unknown as typeof globalThis.fetch;
const store = getDebugProxyCaptureStore();
store.upsertSession({
id: "ms-voices-global-session",
startedAt: Date.now(),
mode: "test",
sourceScope: "openclaw",
sourceProcess: "openclaw",
});
initializeDebugProxyCapture("test");
try {
await listMicrosoftVoices();
let events: Array<Record<string, unknown>> = [];
await vi.waitFor(() => {
events = store
.getSessionEvents("ms-voices-global-session", 10)
.filter((event) => event.host === "speech.platform.bing.com");
expect(events).toHaveLength(2);
});
const kinds = events.map((event) => String(event.kind)).toSorted();
expect(kinds).toEqual(["request", "response"]);
} finally {
globalThis.fetch = proxyReset.originalFetch;
finalizeDebugProxyCapture();
}
});
});
describe("isCjkDominant", () => {
it("returns true for Chinese text", () => {
expect(isCjkDominant("你好世界")).toBe(true);
});
it("returns true for mixed text with majority CJK", () => {
expect(isCjkDominant("你好,这是一个测试 hello")).toBe(true);
});
it("returns false for English text", () => {
expect(isCjkDominant("Hello, this is a test")).toBe(false);
});
it("returns false for empty string", () => {
expect(isCjkDominant("")).toBe(false);
});
it("returns false for mostly English with a few CJK chars", () => {
expect(isCjkDominant("This is a long English sentence with one 字")).toBe(false);
});
});
describe("buildMicrosoftSpeechProvider", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("switches to a Chinese voice for CJK text when no explicit voice override is set", async () => {
const provider = buildMicrosoftSpeechProvider();
const edgeSpy = vi.spyOn(ttsModule, "edgeTTS").mockImplementation(async ({ outputPath }) => {
writeFileSync(outputPath, Buffer.from([0xff, 0xfb, 0x90, 0x00]));
});
await provider.synthesize({
text: "你好,这是一个测试 hello",
cfg: TEST_CFG,
providerConfig: {
enabled: true,
voice: "en-US-MichelleNeural",
lang: "en-US",
outputFormat: "audio-24khz-48kbitrate-mono-mp3",
outputFormatConfigured: true,
saveSubtitles: false,
},
providerOverrides: {},
timeoutMs: 1000,
target: "audio-file",
});
expect(edgeSpy).toHaveBeenCalledOnce();
const edgeCall = requireFirstEdgeTtsCall(edgeSpy);
expect(edgeCall.text).toBe("你好,这是一个测试 hello");
expect(path.basename(edgeCall.outputPath)).toBe("speech.mp3");
expect(edgeCall.timeoutMs).toBe(1000);
expect(edgeCall.config).toEqual({
enabled: true,
voice: "zh-CN-XiaoxiaoNeural",
lang: "zh-CN",
outputFormat: "audio-24khz-48kbitrate-mono-mp3",
outputFormatConfigured: true,
pitch: undefined,
rate: undefined,
volume: undefined,
saveSubtitles: false,
proxy: undefined,
timeoutMs: undefined,
});
});
it("preserves an explicitly configured English voice for CJK text", async () => {
const provider = buildMicrosoftSpeechProvider();
const edgeSpy = vi.spyOn(ttsModule, "edgeTTS").mockImplementation(async ({ outputPath }) => {
writeFileSync(outputPath, Buffer.from([0xff, 0xfb, 0x90, 0x00]));
});
await provider.synthesize({
text: "你好,这是一个测试 hello",
cfg: TEST_CFG,
providerConfig: {
enabled: true,
voice: "en-US-AvaNeural",
lang: "en-US",
outputFormat: "audio-24khz-48kbitrate-mono-mp3",
outputFormatConfigured: true,
saveSubtitles: false,
},
providerOverrides: {},
timeoutMs: 1000,
target: "audio-file",
});
expect(edgeSpy).toHaveBeenCalledOnce();
const edgeCall = requireFirstEdgeTtsCall(edgeSpy);
expect(edgeCall.text).toBe("你好,这是一个测试 hello");
expect(path.basename(edgeCall.outputPath)).toBe("speech.mp3");
expect(edgeCall.timeoutMs).toBe(1000);
expect(edgeCall.config).toEqual({
enabled: true,
voice: "en-US-AvaNeural",
lang: "en-US",
outputFormat: "audio-24khz-48kbitrate-mono-mp3",
outputFormatConfigured: true,
pitch: undefined,
rate: undefined,
volume: undefined,
saveSubtitles: false,
proxy: undefined,
timeoutMs: undefined,
});
});
});

View File

@@ -0,0 +1,302 @@
// Microsoft provider module implements model/runtime integration.
import { readFileSync } from "node:fs";
import path from "node:path";
import {
CHROMIUM_FULL_VERSION,
TRUSTED_CLIENT_TOKEN,
generateSecMsGecToken,
} from "node-edge-tts/dist/drm.js";
import { isVoiceCompatibleAudio } from "openclaw/plugin-sdk/media-runtime";
import {
assertOkOrThrowProviderError,
readProviderJsonResponse,
} from "openclaw/plugin-sdk/provider-http";
import {
captureHttpExchange,
isDebugProxyGlobalFetchPatchInstalled,
} from "openclaw/plugin-sdk/proxy-capture";
import type {
SpeechProviderConfig,
SpeechProviderPlugin,
SpeechVoiceOption,
} from "openclaw/plugin-sdk/speech";
import { asBoolean, asFiniteNumber, asObject, trimToUndefined } from "openclaw/plugin-sdk/speech";
import {
fetchWithSsrFGuard,
ssrfPolicyFromHttpBaseUrlAllowedHostname,
} from "openclaw/plugin-sdk/ssrf-runtime";
import { tempWorkspace, resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
import { edgeTTS, inferEdgeExtension } from "./tts.js";
const DEFAULT_EDGE_VOICE = "en-US-MichelleNeural";
const DEFAULT_EDGE_LANG = "en-US";
const DEFAULT_EDGE_OUTPUT_FORMAT = "audio-24khz-48kbitrate-mono-mp3";
type MicrosoftProviderConfig = {
enabled: boolean;
voice: string;
lang: string;
outputFormat: string;
outputFormatConfigured: boolean;
pitch?: string;
rate?: string;
volume?: string;
saveSubtitles: boolean;
proxy?: string;
timeoutMs?: number;
};
type MicrosoftVoiceListEntry = {
ShortName?: string;
FriendlyName?: string;
Locale?: string;
Gender?: string;
VoiceTag?: {
ContentCategories?: string[];
VoicePersonalities?: string[];
};
};
function normalizeMicrosoftProviderConfig(
rawConfig: Record<string, unknown>,
): MicrosoftProviderConfig {
const providers = asObject(rawConfig.providers);
const rawEdge = asObject(rawConfig.edge);
const rawMicrosoft = asObject(rawConfig.microsoft);
const rawProviderMicrosoft = asObject(providers?.microsoft);
const raw = { ...rawEdge, ...rawMicrosoft, ...rawProviderMicrosoft };
const outputFormat = trimToUndefined(raw.outputFormat);
return {
enabled: asBoolean(raw.enabled) ?? true,
voice: trimToUndefined(raw.voice) ?? DEFAULT_EDGE_VOICE,
lang: trimToUndefined(raw.lang) ?? DEFAULT_EDGE_LANG,
outputFormat: outputFormat ?? DEFAULT_EDGE_OUTPUT_FORMAT,
outputFormatConfigured: Boolean(outputFormat),
pitch: trimToUndefined(raw.pitch),
rate: trimToUndefined(raw.rate),
volume: trimToUndefined(raw.volume),
saveSubtitles: asBoolean(raw.saveSubtitles) ?? false,
proxy: trimToUndefined(raw.proxy),
timeoutMs: asFiniteNumber(raw.timeoutMs),
};
}
function readMicrosoftProviderConfig(config: SpeechProviderConfig): MicrosoftProviderConfig {
const defaults = normalizeMicrosoftProviderConfig({});
return {
enabled: asBoolean(config.enabled) ?? defaults.enabled,
voice: trimToUndefined(config.voice) ?? defaults.voice,
lang: trimToUndefined(config.lang) ?? defaults.lang,
outputFormat: trimToUndefined(config.outputFormat) ?? defaults.outputFormat,
outputFormatConfigured:
asBoolean(config.outputFormatConfigured) ?? defaults.outputFormatConfigured,
pitch: trimToUndefined(config.pitch) ?? defaults.pitch,
rate: trimToUndefined(config.rate) ?? defaults.rate,
volume: trimToUndefined(config.volume) ?? defaults.volume,
saveSubtitles: asBoolean(config.saveSubtitles) ?? defaults.saveSubtitles,
proxy: trimToUndefined(config.proxy) ?? defaults.proxy,
timeoutMs: asFiniteNumber(config.timeoutMs) ?? defaults.timeoutMs,
};
}
function buildMicrosoftVoiceHeaders(): Record<string, string> {
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 formatMicrosoftVoiceDescription(entry: MicrosoftVoiceListEntry): string | undefined {
const personalities = entry.VoiceTag?.VoicePersonalities?.filter(Boolean) ?? [];
return personalities.length > 0 ? personalities.join(", ") : undefined;
}
export function isCjkDominant(text: string): boolean {
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 >= 0x4e00 && code <= 0x9fff) ||
(code >= 0x3400 && code <= 0x4dbf) ||
(code >= 0x3000 && code <= 0x303f) ||
(code >= 0xff00 && code <= 0xffef)
) {
cjkCount += 1;
}
}
return cjkCount / stripped.length > 0.3;
}
const DEFAULT_CHINESE_EDGE_VOICE = "zh-CN-XiaoxiaoNeural";
const DEFAULT_CHINESE_EDGE_LANG = "zh-CN";
export async function listMicrosoftVoices(): Promise<SpeechVoiceOption[]> {
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",
});
try {
if (!isDebugProxyGlobalFetchPatchInstalled()) {
captureHttpExchange({
url,
method: "GET",
requestHeaders: headers,
response,
transport: "http",
meta: {
provider: "microsoft",
capability: "speech-voices",
},
});
}
await assertOkOrThrowProviderError(response, "Microsoft voices API error");
const voices = await readProviderJsonResponse<MicrosoftVoiceListEntry[]>(
response,
"microsoft.speech-voices",
);
return Array.isArray(voices)
? voices
.map((voice) => ({
id: voice.ShortName?.trim() ?? "",
name: trimToUndefined(voice.FriendlyName) ?? trimToUndefined(voice.ShortName),
category: voice.VoiceTag?.ContentCategories?.find((value) => value.trim().length > 0),
description: formatMicrosoftVoiceDescription(voice),
locale: trimToUndefined(voice.Locale),
gender: trimToUndefined(voice.Gender),
personalities: voice.VoiceTag?.VoicePersonalities?.filter(
(value): value is string => value.trim().length > 0,
),
}))
.filter((voice) => voice.id.length > 0)
: [];
} finally {
await release();
}
}
export function buildMicrosoftSpeechProvider(): SpeechProviderPlugin {
return {
id: "microsoft",
label: "Microsoft",
aliases: ["edge"],
autoSelectOrder: 30,
resolveConfig: ({ rawConfig }) => normalizeMicrosoftProviderConfig(rawConfig),
resolveTalkConfig: ({ baseTtsConfig, talkProviderConfig }) => {
const base = normalizeMicrosoftProviderConfig(baseTtsConfig);
return {
...base,
enabled: true,
...(trimToUndefined(talkProviderConfig.voiceId) == null
? {}
: { voice: trimToUndefined(talkProviderConfig.voiceId) }),
...(trimToUndefined(talkProviderConfig.languageCode) == null
? {}
: { lang: trimToUndefined(talkProviderConfig.languageCode) }),
...(trimToUndefined(talkProviderConfig.outputFormat) == null
? {}
: { outputFormat: trimToUndefined(talkProviderConfig.outputFormat) }),
...(trimToUndefined(talkProviderConfig.pitch) == null
? {}
: { pitch: trimToUndefined(talkProviderConfig.pitch) }),
...(trimToUndefined(talkProviderConfig.rate) == null
? {}
: { rate: trimToUndefined(talkProviderConfig.rate) }),
...(trimToUndefined(talkProviderConfig.volume) == null
? {}
: { volume: trimToUndefined(talkProviderConfig.volume) }),
...(trimToUndefined(talkProviderConfig.proxy) == null
? {}
: { proxy: trimToUndefined(talkProviderConfig.proxy) }),
...(asFiniteNumber(talkProviderConfig.timeoutMs) == null
? {}
: { timeoutMs: asFiniteNumber(talkProviderConfig.timeoutMs) }),
};
},
resolveTalkOverrides: ({ params }) => ({
...(trimToUndefined(params.voiceId) == null
? {}
: { voice: trimToUndefined(params.voiceId) }),
...(trimToUndefined(params.outputFormat) == null
? {}
: { outputFormat: trimToUndefined(params.outputFormat) }),
}),
listVoices: async () => await listMicrosoftVoices(),
isConfigured: ({ providerConfig }) => readMicrosoftProviderConfig(providerConfig).enabled,
synthesize: async (req) => {
const config = readMicrosoftProviderConfig(req.providerConfig);
const temp = await tempWorkspace({
rootDir: resolvePreferredOpenClawTmpDir(),
prefix: "tts-microsoft-",
});
const tempDir = temp.dir;
const overrideVoice = trimToUndefined(req.providerOverrides?.voice);
let voice = overrideVoice ?? config.voice;
let lang = config.lang;
let outputFormat =
trimToUndefined(req.providerOverrides?.outputFormat) ?? config.outputFormat;
const fallbackOutputFormat =
outputFormat !== DEFAULT_EDGE_OUTPUT_FORMAT ? DEFAULT_EDGE_OUTPUT_FORMAT : undefined;
if (!overrideVoice && voice === DEFAULT_EDGE_VOICE && isCjkDominant(req.text)) {
voice = DEFAULT_CHINESE_EDGE_VOICE;
lang = DEFAULT_CHINESE_EDGE_LANG;
}
try {
const runEdge = async (format: string) => {
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,
});
const audioBuffer = readFileSync(outputPath);
return {
audioBuffer,
outputFormat: format,
fileExtension,
voiceCompatible: isVoiceCompatibleAudio({ fileName: outputPath }),
};
};
try {
return await runEdge(outputFormat);
} catch (error) {
if (!fallbackOutputFormat || fallbackOutputFormat === outputFormat) {
throw error;
}
outputFormat = fallbackOutputFormat;
return await runEdge(outputFormat);
}
} finally {
await temp.cleanup();
}
},
};
}

View File

@@ -0,0 +1,2 @@
// Microsoft API module exposes the plugin public contract.
export { buildMicrosoftSpeechProvider } from "./speech-provider.js";

View File

@@ -0,0 +1,16 @@
{
"extends": "../tsconfig.package-boundary.base.json",
"compilerOptions": {
"rootDir": "."
},
"include": ["./*.ts", "./src/**/*.ts"],
"exclude": [
"./**/*.test.ts",
"./dist/**",
"./node_modules/**",
"./src/test-support/**",
"./src/**/*test-helpers.ts",
"./src/**/*test-harness.ts",
"./src/**/*test-support.ts"
]
}

View File

@@ -0,0 +1,194 @@
// Microsoft tests cover tts plugin behavior.
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
let edgeTTS: typeof import("./tts.js").edgeTTS;
function createEdgeTTSDeps(
ttsPromise: (text: string, filePath: string) => Promise<void>,
onConstruct?: () => void,
) {
return {
EdgeTTS: class {
constructor() {
onConstruct?.();
}
ttsPromise(text: string, filePath: string) {
return ttsPromise(text, filePath);
}
},
};
}
const baseEdgeConfig = {
voice: "en-US-MichelleNeural",
lang: "en-US",
outputFormat: "audio-24khz-48kbitrate-mono-mp3",
saveSubtitles: false,
};
describe("edgeTTS empty audio validation", () => {
let tempDir: string | undefined;
beforeAll(async () => {
({ edgeTTS } = await import("./tts.js"));
});
afterEach(() => {
if (tempDir) {
rmSync(tempDir, { recursive: true, force: true });
tempDir = undefined;
}
});
it("rejects blank text before constructing Edge TTS", async () => {
tempDir = mkdtempSync(path.join(tmpdir(), "tts-test-"));
const outputPath = path.join(tempDir, "voice.mp3");
const onConstruct = vi.fn();
const deps = createEdgeTTSDeps(async (_text: string, filePath: string) => {
writeFileSync(filePath, Buffer.from([0xff]));
}, onConstruct);
await expect(
edgeTTS(
{
text: " \n\t ",
outputPath,
config: baseEdgeConfig,
timeoutMs: 10000,
},
deps,
),
).rejects.toThrow("Microsoft TTS text cannot be empty");
expect(onConstruct).not.toHaveBeenCalled();
});
it("throws after one retry when the output file stays empty", async () => {
tempDir = mkdtempSync(path.join(tmpdir(), "tts-test-"));
const outputPath = path.join(tempDir, "voice.mp3");
const calls: string[] = [];
const deps = createEdgeTTSDeps(async (text: string, filePath: string) => {
calls.push(text);
writeFileSync(filePath, "");
});
await expect(
edgeTTS(
{
text: "Hello",
outputPath,
config: baseEdgeConfig,
timeoutMs: 10000,
},
deps,
),
).rejects.toThrow("Edge TTS produced empty audio file after retry");
expect(calls).toEqual(["Hello", "Hello"]);
});
it("succeeds when the output file has content", async () => {
tempDir = mkdtempSync(path.join(tmpdir(), "tts-test-"));
const outputPath = path.join(tempDir, "voice.mp3");
let stagedPath = "";
const deps = createEdgeTTSDeps(async (_text: string, filePath: string) => {
stagedPath = filePath;
writeFileSync(filePath, Buffer.from([0xff, 0xfb, 0x90, 0x00]));
});
await expect(
edgeTTS(
{
text: "Hello",
outputPath,
config: baseEdgeConfig,
timeoutMs: 10000,
},
deps,
),
).resolves.toBeUndefined();
expect(stagedPath).not.toBe(outputPath);
expect(path.basename(stagedPath)).toContain(path.basename(outputPath));
expect(path.basename(stagedPath)).toMatch(/\.part$/);
expect(readFileSync(outputPath)).toEqual(Buffer.from([0xff, 0xfb, 0x90, 0x00]));
expect(existsSync(stagedPath)).toBe(false);
});
it("retries once when the first output file is empty", async () => {
tempDir = mkdtempSync(path.join(tmpdir(), "tts-test-"));
const outputPath = path.join(tempDir, "voice.mp3");
const calls: string[] = [];
const deps = createEdgeTTSDeps(async (text: string, filePath: string) => {
calls.push(text);
writeFileSync(filePath, calls.length === 1 ? "" : Buffer.from([0xff, 0xfb, 0x90, 0x00]));
});
await expect(
edgeTTS(
{
text: "Hello",
outputPath,
config: baseEdgeConfig,
timeoutMs: 10000,
},
deps,
),
).resolves.toBeUndefined();
expect(calls).toEqual(["Hello", "Hello"]);
});
it("retries once when Edge TTS resolves without creating an output file", async () => {
tempDir = mkdtempSync(path.join(tmpdir(), "tts-test-"));
const outputPath = path.join(tempDir, "voice.mp3");
const calls: string[] = [];
const deps = createEdgeTTSDeps(async (text: string, filePath: string) => {
calls.push(text);
if (calls.length === 2) {
writeFileSync(filePath, Buffer.from([0xff, 0xfb, 0x90, 0x00]));
}
});
await expect(
edgeTTS(
{
text: "Hello",
outputPath,
config: baseEdgeConfig,
timeoutMs: 10000,
},
deps,
),
).resolves.toBeUndefined();
expect(calls).toEqual(["Hello", "Hello"]);
});
it("does not retry provider errors", async () => {
tempDir = mkdtempSync(path.join(tmpdir(), "tts-test-"));
const outputPath = path.join(tempDir, "voice.mp3");
const calls: string[] = [];
const deps = createEdgeTTSDeps(async (text: string) => {
calls.push(text);
throw new Error("upstream timeout");
});
await expect(
edgeTTS(
{
text: "Hello",
outputPath,
config: baseEdgeConfig,
timeoutMs: 10000,
},
deps,
),
).rejects.toThrow("upstream timeout");
expect(calls).toEqual(["Hello"]);
});
});

138
extensions/microsoft/tts.ts Normal file
View File

@@ -0,0 +1,138 @@
// Microsoft plugin module implements tts behavior.
import { statSync, writeFileSync } from "node:fs";
import { mkdir } from "node:fs/promises";
import path from "node:path";
import { writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
type EdgeTTSRuntimeConfig = {
voice?: string;
lang?: string;
outputFormat?: string;
saveSubtitles?: boolean;
proxy?: string;
rate?: string;
pitch?: string;
volume?: string;
timeout?: number;
};
type EdgeTTSDeps = {
EdgeTTS: new (config: EdgeTTSRuntimeConfig) => {
ttsPromise: (text: string, outputPath: string) => Promise<unknown>;
};
};
async function loadDefaultEdgeTTSDeps(): Promise<EdgeTTSDeps> {
const { EdgeTTS } = await import("node-edge-tts");
return { EdgeTTS };
}
function isMissingOutputFileError(error: unknown): boolean {
return (
typeof error === "object" &&
error !== null &&
"code" in error &&
(error as { code?: unknown }).code === "ENOENT"
);
}
function readOutputSize(outputPath: string): number {
try {
return statSync(outputPath).size;
} catch (error) {
if (isMissingOutputFileError(error)) {
return 0;
}
throw error;
}
}
export function inferEdgeExtension(outputFormat: string): string {
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";
}
export async function edgeTTS(
params: {
text: string;
outputPath: string;
config: {
voice: string;
lang: string;
outputFormat: string;
saveSubtitles: boolean;
proxy?: string;
rate?: string;
pitch?: string;
volume?: string;
timeoutMs?: number;
};
timeoutMs: number;
},
deps?: EdgeTTSDeps,
): Promise<void> {
const { text, outputPath, config, timeoutMs } = params;
if (text.trim().length === 0) {
throw new Error("Microsoft TTS text cannot be empty");
}
const resolvedDeps = deps ?? (await loadDefaultEdgeTTSDeps());
const tts = new resolvedDeps.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,
});
for (let attempt = 0; attempt < 2; attempt += 1) {
const outputSize = await writeEdgeTtsOutput({
outputPath,
ttsPromise: async (tempPath) => {
await tts.ttsPromise(text, tempPath);
},
});
if (outputSize > 0) {
return;
}
}
throw new Error("Edge TTS produced empty audio file after retry");
}
async function writeEdgeTtsOutput(params: {
outputPath: string;
ttsPromise: (tempPath: string) => Promise<void>;
}): Promise<number> {
const rootDir = path.dirname(params.outputPath);
await mkdir(rootDir, { recursive: true });
let outputSize = 0;
await writeExternalFileWithinRoot({
rootDir,
path: path.basename(params.outputPath),
write: async (tempPath) => {
await params.ttsPromise(tempPath);
outputSize = readOutputSize(tempPath);
if (outputSize === 0) {
writeFileSync(tempPath, "");
}
},
});
return outputSize;
}