feat(tts): enrich speech voice metadata
This commit is contained in:
parent
5f5b409fe9
commit
57f1ab1fca
@ -110,6 +110,39 @@ describe("talk-voice plugin", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("surfaces richer provider voice metadata when available", async () => {
|
||||
const { command, runtime } = createHarness({
|
||||
talk: {
|
||||
provider: "microsoft",
|
||||
providers: {
|
||||
microsoft: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
vi.mocked(runtime.tts.listVoices).mockResolvedValue([
|
||||
{
|
||||
id: "en-US-AvaNeural",
|
||||
name: "Ava",
|
||||
category: "General",
|
||||
locale: "en-US",
|
||||
gender: "Female",
|
||||
personalities: ["Friendly", "Positive"],
|
||||
description: "Friendly, Positive",
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await command.handler(createCommandContext("list"));
|
||||
|
||||
expect(result).toEqual({
|
||||
text:
|
||||
"Microsoft voices: 1\n\n" +
|
||||
"- Ava · General\n" +
|
||||
" id: en-US-AvaNeural\n" +
|
||||
" meta: en-US · Female · Friendly, Positive\n" +
|
||||
" note: Friendly, Positive",
|
||||
});
|
||||
});
|
||||
|
||||
it("writes canonical talk provider config and legacy elevenlabs voice id", async () => {
|
||||
const { command, runtime } = createHarness({
|
||||
talk: {
|
||||
|
||||
@ -31,6 +31,16 @@ function resolveProviderLabel(providerId: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function formatVoiceMeta(voice: SpeechVoiceOption): string | undefined {
|
||||
const parts = [voice.locale, voice.gender];
|
||||
const personalities = voice.personalities?.filter((value) => value.trim().length > 0) ?? [];
|
||||
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;
|
||||
}
|
||||
|
||||
function formatVoiceList(voices: SpeechVoiceOption[], limit: number, providerId: string): string {
|
||||
const sliced = voices.slice(0, Math.max(1, Math.min(limit, 50)));
|
||||
const lines: string[] = [];
|
||||
@ -42,6 +52,14 @@ function formatVoiceList(voices: SpeechVoiceOption[], limit: number, providerId:
|
||||
const meta = category ? ` · ${category}` : "";
|
||||
lines.push(`- ${name}${meta}`);
|
||||
lines.push(` id: ${v.id}`);
|
||||
const details = formatVoiceMeta(v);
|
||||
if (details) {
|
||||
lines.push(` meta: ${details}`);
|
||||
}
|
||||
const description = (v.description ?? "").trim();
|
||||
if (description) {
|
||||
lines.push(` note: ${description}`);
|
||||
}
|
||||
}
|
||||
if (voices.length > sliced.length) {
|
||||
lines.push("");
|
||||
|
||||
@ -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));
|
||||
});
|
||||
});
|
||||
|
||||
@ -42,6 +42,9 @@ export type SpeechVoiceOption = {
|
||||
name?: string;
|
||||
category?: string;
|
||||
description?: string;
|
||||
locale?: string;
|
||||
gender?: string;
|
||||
personalities?: string[];
|
||||
};
|
||||
|
||||
export type SpeechListVoicesRequest = {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
: [];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user