45 lines
1.9 KiB
TypeScript
45 lines
1.9 KiB
TypeScript
import { z } from "zod";
|
|
import { generateSecMsGecToken_V2, SEC_MS_GEC_VERSION_V2, TRUSTED_CLIENT_TOKEN } from "node-edge-tts/dist/drm";
|
|
import { speechVoiceSchema, type SpeechVoice } from "../src/shared/tts/voices";
|
|
|
|
const upstreamVoicesSchema = z.array(z.object({
|
|
ShortName: z.string().min(1).max(120),
|
|
FriendlyName: z.string().min(1).max(200),
|
|
Locale: z.string().min(2).max(40),
|
|
Gender: z.enum(["Female", "Male"])
|
|
})).min(1).max(2_000);
|
|
|
|
export async function fetchEdgeTtsVoices(): Promise<SpeechVoice[]> {
|
|
const url = new URL("https://speech.platform.bing.com/consumer/speech/synthesize/readaloud/voices/list");
|
|
url.searchParams.set("trustedclienttoken", TRUSTED_CLIENT_TOKEN);
|
|
url.searchParams.set("Sec-MS-GEC", generateSecMsGecToken_V2());
|
|
url.searchParams.set("Sec-MS-GEC-Version", SEC_MS_GEC_VERSION_V2);
|
|
const response = await fetch(url, { signal: AbortSignal.timeout(10_000) });
|
|
if (!response.ok) throw new Error(`Edge TTS voice catalog returned ${response.status}`);
|
|
const voices = upstreamVoicesSchema.parse(await response.json()).filter((voice) => voice.Locale === "zh-CN");
|
|
if (!voices.length) throw new Error("Edge TTS returned no Mandarin voices");
|
|
return voices.map((voice) => speechVoiceSchema.parse({
|
|
id: voice.ShortName, name: voice.FriendlyName, locale: voice.Locale, gender: voice.Gender
|
|
}));
|
|
}
|
|
|
|
export function createEdgeTtsVoiceCatalog(
|
|
load: () => Promise<SpeechVoice[]> = fetchEdgeTtsVoices,
|
|
now: () => number = Date.now
|
|
) {
|
|
let cached: SpeechVoice[] | undefined;
|
|
let expiresAt = 0;
|
|
let pending: Promise<SpeechVoice[]> | undefined;
|
|
return () => {
|
|
if (cached && now() < expiresAt) return Promise.resolve(cached);
|
|
if (!pending) {
|
|
pending = Promise.resolve().then(load).then((voices) => {
|
|
cached = voices;
|
|
expiresAt = now() + 60 * 60 * 1_000;
|
|
return voices;
|
|
}).finally(() => { pending = undefined; });
|
|
}
|
|
return pending;
|
|
};
|
|
}
|