feat(tts): enrich speech voice metadata

This commit is contained in:
Peter Steinberger
2026-03-16 20:27:25 -07:00
parent 5f5b409fe9
commit 57f1ab1fca
6 changed files with 78 additions and 7 deletions

View File

@@ -27,6 +27,14 @@ function findSpeechProviderIdsForPlugin(pluginId: string) {
.toSorted((left, right) => left.localeCompare(right));
}
function findSpeechProviderForPlugin(pluginId: string) {
const entry = speechProviderContractRegistry.find((candidate) => candidate.pluginId === pluginId);
if (!entry) {
throw new Error(`speech provider contract missing for ${pluginId}`);
}
return entry.provider;
}
function findRegistrationForPlugin(pluginId: string) {
const entry = pluginRegistrationContractRegistry.find(
(candidate) => candidate.pluginId === pluginId,
@@ -97,4 +105,10 @@ describe("plugin contract registry", () => {
speechProviderIds: ["microsoft"],
});
});
it("keeps bundled speech voice-list support explicit", () => {
expect(findSpeechProviderForPlugin("openai").listVoices).toEqual(expect.any(Function));
expect(findSpeechProviderForPlugin("elevenlabs").listVoices).toEqual(expect.any(Function));
expect(findSpeechProviderForPlugin("microsoft").listVoices).toEqual(expect.any(Function));
});
});

View File

@@ -42,6 +42,9 @@ export type SpeechVoiceOption = {
name?: string;
category?: string;
description?: string;
locale?: string;
gender?: string;
personalities?: string[];
};
export type SpeechListVoicesRequest = {

View File

@@ -35,7 +35,10 @@ describe("listMicrosoftVoices", () => {
id: "en-US-AvaNeural",
name: "Microsoft Ava Online (Natural) - English (United States)",
category: "General",
description: "en-US · Female · Friendly, Positive",
description: "Friendly, Positive",
locale: "en-US",
gender: "Female",
personalities: ["Friendly", "Positive"],
},
]);
expect(globalThis.fetch).toHaveBeenCalledWith(

View File

@@ -39,13 +39,8 @@ function buildMicrosoftVoiceHeaders(): Record<string, string> {
}
function formatMicrosoftVoiceDescription(entry: MicrosoftVoiceListEntry): string | undefined {
const parts = [entry.Locale, entry.Gender];
const personalities = entry.VoiceTag?.VoicePersonalities?.filter(Boolean) ?? [];
if (personalities.length > 0) {
parts.push(personalities.join(", "));
}
const filtered = parts.filter((part): part is string => Boolean(part?.trim()));
return filtered.length > 0 ? filtered.join(" · ") : undefined;
return personalities.length > 0 ? personalities.join(", ") : undefined;
}
export async function listMicrosoftVoices(): Promise<SpeechVoiceOption[]> {
@@ -67,6 +62,11 @@ export async function listMicrosoftVoices(): Promise<SpeechVoiceOption[]> {
name: voice.FriendlyName?.trim() || voice.ShortName?.trim() || undefined,
category: voice.VoiceTag?.ContentCategories?.find((value) => value.trim().length > 0),
description: formatMicrosoftVoiceDescription(voice),
locale: voice.Locale?.trim() || undefined,
gender: voice.Gender?.trim() || undefined,
personalities: voice.VoiceTag?.VoicePersonalities?.filter(
(value): value is string => value.trim().length > 0,
),
}))
.filter((voice) => voice.id.length > 0)
: [];