Files
next-tjwater-frontend/server/edge-tts-voices.ts
T
jiang c0e4231b83
Generic Container CI/CD / test-build-publish (push) Successful in 1m52s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 1m52s
feat: expand intelligent investigation workspace
2026-10-30 17:30:19 +08:00

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;
};
}