From 1e3b76f26b079f076f996dac6d196ef7dc2f9479 Mon Sep 17 00:00:00 2001
From: Alix-007
Date: Tue, 21 Jul 2026 09:56:52 +0800
Subject: [PATCH] fix(microsoft): keep valid voices from malformed catalogs
(#110784)
---
extensions/microsoft/speech-provider.test.ts | 46 ++++++++++++++++
extensions/microsoft/speech-provider.ts | 57 +++++++++-----------
2 files changed, 72 insertions(+), 31 deletions(-)
diff --git a/extensions/microsoft/speech-provider.test.ts b/extensions/microsoft/speech-provider.test.ts
index 729c4840eacf..1f340c5040bf 100644
--- a/extensions/microsoft/speech-provider.test.ts
+++ b/extensions/microsoft/speech-provider.test.ts
@@ -105,6 +105,52 @@ describe("listMicrosoftVoices", () => {
);
});
+ it("returns an empty catalog for a malformed top-level payload", async () => {
+ globalThis.fetch = vi
+ .fn()
+ .mockResolvedValue(
+ new Response("null", { status: 200 }),
+ ) as unknown as typeof globalThis.fetch;
+
+ await expect(listVoicesThroughProvider()).resolves.toEqual([]);
+ });
+
+ it("skips malformed rows without discarding valid voices", async () => {
+ globalThis.fetch = vi.fn().mockResolvedValue(
+ new Response(
+ JSON.stringify([
+ null,
+ "unexpected",
+ [],
+ { ShortName: 42 },
+ {
+ ShortName: "en-US-AvaNeural",
+ FriendlyName: "Microsoft Ava Online (Natural) - English (United States)",
+ Locale: "en-US",
+ Gender: "Female",
+ VoiceTag: {
+ ContentCategories: [null, "General"],
+ VoicePersonalities: [false, "Friendly", "Positive"],
+ },
+ },
+ ]),
+ { status: 200 },
+ ),
+ ) as unknown as typeof globalThis.fetch;
+
+ await expect(listVoicesThroughProvider()).resolves.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()
diff --git a/extensions/microsoft/speech-provider.ts b/extensions/microsoft/speech-provider.ts
index 8f4f45f4a078..508b5d9bbe07 100644
--- a/extensions/microsoft/speech-provider.ts
+++ b/extensions/microsoft/speech-provider.ts
@@ -47,17 +47,6 @@ type MicrosoftProviderConfig = {
timeoutMs?: number;
};
-type MicrosoftVoiceListEntry = {
- ShortName?: string;
- FriendlyName?: string;
- Locale?: string;
- Gender?: string;
- VoiceTag?: {
- ContentCategories?: string[];
- VoicePersonalities?: string[];
- };
-};
-
function normalizeMicrosoftProviderConfig(
rawConfig: Record,
): MicrosoftProviderConfig {
@@ -114,9 +103,10 @@ function buildMicrosoftVoiceHeaders(): Record {
};
}
-function formatMicrosoftVoiceDescription(entry: MicrosoftVoiceListEntry): string | undefined {
- const personalities = entry.VoiceTag?.VoicePersonalities?.filter(Boolean) ?? [];
- return personalities.length > 0 ? personalities.join(", ") : undefined;
+function readMicrosoftVoiceTagStrings(value: unknown): string[] | undefined {
+ return Array.isArray(value)
+ ? value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0)
+ : undefined;
}
function isCjkDominant(text: string): boolean {
@@ -173,24 +163,29 @@ async function listMicrosoftVoices(
});
}
await assertOkOrThrowProviderError(response, "Microsoft voices API error");
- const voices = await readProviderJsonResponse(
- response,
- "microsoft.speech-voices",
- );
+ const voices = await readProviderJsonResponse(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)
+ ? voices.flatMap((value) => {
+ const voice = asObject(value);
+ const id = trimToUndefined(voice?.ShortName);
+ if (!voice || !id) {
+ return [];
+ }
+ const voiceTag = asObject(voice.VoiceTag);
+ const categories = readMicrosoftVoiceTagStrings(voiceTag?.ContentCategories);
+ const personalities = readMicrosoftVoiceTagStrings(voiceTag?.VoicePersonalities);
+ return [
+ {
+ id,
+ name: trimToUndefined(voice.FriendlyName) ?? id,
+ category: categories?.[0],
+ description: personalities?.length ? personalities.join(", ") : undefined,
+ locale: trimToUndefined(voice.Locale),
+ gender: trimToUndefined(voice.Gender),
+ personalities,
+ },
+ ];
+ })
: [];
} finally {
await release();