From 2e98ed44ee95cb32d20b5fa9d65b27a82a34929b Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Mon, 18 Aug 2025 20:44:38 +0800 Subject: [PATCH] tts: fix edge tts, closes #1821 (#1828) --- apps/readest-app/src/libs/edgeTTS.ts | 35 +++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/apps/readest-app/src/libs/edgeTTS.ts b/apps/readest-app/src/libs/edgeTTS.ts index 79416785..73decfb1 100644 --- a/apps/readest-app/src/libs/edgeTTS.ts +++ b/apps/readest-app/src/libs/edgeTTS.ts @@ -1,4 +1,5 @@ import { md5 } from 'js-md5'; +import { createHash } from 'crypto'; import { randomMd5 } from '@/utils/misc'; import { LRUCache } from '@/utils/lru'; import { genSSML } from '@/utils/ssml'; @@ -6,6 +7,7 @@ import { genSSML } from '@/utils/ssml'; const EDGE_SPEECH_URL = 'wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1'; const EDGE_API_TOKEN = '6A5AA1D4EAFF4E9FB37E23D68491D6F4'; +const CHROMIUM_FULL_VERSION = '130.0.2849.68'; const EDGE_TTS_VOICES = { 'af-ZA': ['af-ZA-AdriNeural', 'af-ZA-WillemNeural'], 'am-ET': ['am-ET-AmehaNeural', 'am-ET-MekdesNeural'], @@ -120,6 +122,31 @@ const EDGE_TTS_VOICES = { 'zh-TW': ['zh-TW-HsiaoChenNeural', 'zh-TW-HsiaoYuNeural', 'zh-TW-YunJheNeural'], }; +/** + * Generates the Sec-MS-GEC token value. + * This function generates a token value based on the current time in Windows file time format + * adjusted for clock skew, and rounded down to the nearest 5 minutes. The token is then hashed + * using SHA256 and returned as an uppercased hex digest. + * + * @returns The generated Sec-MS-GEC token value. + * @see https://github.com/rany2/edge-tts/issues/290#issuecomment-2464956570 + */ +const WIN_EPOCH_OFFSET = 11644473600; // Windows epoch offset in seconds (1601 to 1970) +const S_TO_NS = 1000000000; // Seconds to nanoseconds conversion +const generateSecMsGec = () => { + let ticks = Math.floor(Date.now() / 1000); + // Switch to Windows file time epoch (1601-01-01 00:00:00 UTC) + ticks += WIN_EPOCH_OFFSET; + // Round down to the nearest 5 minutes (300 seconds) + ticks -= ticks % 300; + // Convert the ticks to 100-nanosecond intervals (Windows file time format) + ticks *= S_TO_NS / 100; + // Create the string to hash by concatenating the ticks and the trusted client token + const strToHash = `${ticks.toFixed(0)}${EDGE_API_TOKEN}`; + // Compute the SHA256 hash and return the uppercased hex digest + return createHash('sha256').update(strToHash, 'ascii').digest('hex').toUpperCase(); +}; + const genVoiceList = (voices: Record) => { return Object.entries(voices).flatMap(([lang, voices]) => { return voices.map((id) => { @@ -150,7 +177,13 @@ export class EdgeSpeechTTS { async #fetchEdgeSpeechWs({ lang, text, voice, rate }: EdgeTTSPayload): Promise { const connectId = randomMd5(); - const url = `${EDGE_SPEECH_URL}?ConnectionId=${connectId}&TrustedClientToken=${EDGE_API_TOKEN}`; + const params = new URLSearchParams({ + ConnectionId: connectId, + TrustedClientToken: EDGE_API_TOKEN, + 'Sec-MS-GEC': generateSecMsGec(), + 'Sec-MS-GEC-Version': `1-${CHROMIUM_FULL_VERSION}`, + }); + const url = `${EDGE_SPEECH_URL}?${params.toString()}`; const date = new Date().toString(); const configHeaders = { 'Content-Type': 'application/json; charset=utf-8',