fix(microsoft): keep valid voices from malformed catalogs (#110784)

This commit is contained in:
Alix-007
2026-07-21 09:56:52 +08:00
committed by GitHub
parent d4deddb6ed
commit 1e3b76f26b
2 changed files with 72 additions and 31 deletions
@@ -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()
+26 -31
View File
@@ -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<string, unknown>,
): MicrosoftProviderConfig {
@@ -114,9 +103,10 @@ function buildMicrosoftVoiceHeaders(): Record<string, string> {
};
}
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<MicrosoftVoiceListEntry[]>(
response,
"microsoft.speech-voices",
);
const voices = await readProviderJsonResponse<unknown>(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();