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 { 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 = fetchEdgeTtsVoices, now: () => number = Date.now ) { let cached: SpeechVoice[] | undefined; let expiresAt = 0; let pending: Promise | 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; }; }