@oadank/dsh-input-tools 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Microsoft Edge TTS — 原生 WebSocket 客户端(免费,无需 API key)。
3
+ * 从 deepseek-harness packages/host/apiproxy/src/edge-tts.ts 移植(TTS 独立化),
4
+ * 对齐 Python edge-tts v7.2.8 的 DRM + Headers;Sec-MS-GEC 令牌放在 URL 参数里。
5
+ */
6
+
7
+ import WebSocket from 'ws'
8
+ import { createHash, randomBytes } from 'node:crypto'
9
+
10
+ const TRUSTED_CLIENT_TOKEN = '6A5AA1D4EAFF4E9FB37E23D68491D6F4'
11
+ const BASE_URL = 'speech.platform.bing.com/consumer/speech/synthesize/readaloud'
12
+ const CHROMIUM_FULL_VERSION = '143.0.3650.75'
13
+ const CHROMIUM_MAJOR_VERSION = '143'
14
+ const SEC_MS_GEC_VERSION = `1-${CHROMIUM_FULL_VERSION}`
15
+ const WIN_EPOCH = 11644473600
16
+ const S_TO_NS = 1e9
17
+
18
+ function generateSecMsGec() {
19
+ let ticks = Date.now() / 1000
20
+ ticks += WIN_EPOCH
21
+ ticks -= ticks % 300
22
+ ticks *= S_TO_NS / 100
23
+ const strToHash = `${Math.floor(ticks)}${TRUSTED_CLIENT_TOKEN}`
24
+ return createHash('sha256').update(strToHash, 'ascii').digest('hex').toUpperCase()
25
+ }
26
+
27
+ function generateMuid() {
28
+ return randomBytes(16).toString('hex').toUpperCase()
29
+ }
30
+
31
+ function uuid() {
32
+ return crypto.randomUUID().replaceAll('-', '')
33
+ }
34
+
35
+ function escapeXml(s) {
36
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
37
+ .replace(/"/g, '&quot;').replace(/'/g, '&apos;')
38
+ }
39
+
40
+ function getWssUrl() {
41
+ return `wss://${BASE_URL}/edge/v1?TrustedClientToken=${TRUSTED_CLIENT_TOKEN}`
42
+ + `&Sec-MS-GEC=${generateSecMsGec()}&Sec-MS-GEC-Version=${SEC_MS_GEC_VERSION}`
43
+ }
44
+
45
+ function getWssHeaders() {
46
+ return {
47
+ 'Pragma': 'no-cache',
48
+ 'Cache-Control': 'no-cache',
49
+ 'Origin': 'chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold',
50
+ 'User-Agent': `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_MAJOR_VERSION}.0.0.0 Safari/537.36 Edg/${CHROMIUM_MAJOR_VERSION}.0.0.0`,
51
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
52
+ 'Accept-Language': 'en-US,en;q=0.9',
53
+ 'Cookie': `muid=${generateMuid()};`,
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Synthesize speech through the free Microsoft Edge endpoint.
59
+ * @param {string} text - plain text to speak.
60
+ * @param {string} [voice] - Edge voice name (default zh-CN-XiaoxiaoNeural).
61
+ * @returns {Promise<Buffer>} MP3 bytes (audio-24khz-48kbitrate-mono-mp3).
62
+ */
63
+ export function edgeTts(text, voice = 'zh-CN-XiaoxiaoNeural') {
64
+ return new Promise((resolve, reject) => {
65
+ const ws = new WebSocket(getWssUrl(), { headers: getWssHeaders() })
66
+ const audioData = []
67
+ let messageTimeout
68
+
69
+ const connectTimeout = setTimeout(() => {
70
+ ws.terminate()
71
+ reject(new Error('Edge TTS WebSocket connect timeout (10s)'))
72
+ }, 10_000)
73
+
74
+ ws.on('message', (rawData, isBinary) => {
75
+ const buf = rawData
76
+ if (!isBinary) {
77
+ const str = buf.toString('utf8')
78
+ if (str.includes('turn.end')) {
79
+ if (messageTimeout !== undefined) clearTimeout(messageTimeout)
80
+ resolve(Buffer.concat(audioData))
81
+ ws.close()
82
+ }
83
+ return
84
+ }
85
+ const separator = 'Path:audio\r\n'
86
+ const idx = buf.indexOf(separator)
87
+ if (idx !== -1) audioData.push(buf.subarray(idx + separator.length))
88
+ })
89
+
90
+ ws.on('error', (err) => {
91
+ clearTimeout(connectTimeout)
92
+ reject(err)
93
+ })
94
+
95
+ ws.on('open', () => {
96
+ clearTimeout(connectTimeout)
97
+ messageTimeout = setTimeout(() => {
98
+ ws.close()
99
+ reject(new Error('Edge TTS message timeout (30s)'))
100
+ }, 30_000)
101
+
102
+ const speechConfig = JSON.stringify({
103
+ context: { synthesis: { audio: {
104
+ metadataoptions: { sentenceBoundaryEnabled: false, wordBoundaryEnabled: false },
105
+ outputFormat: 'audio-24khz-48kbitrate-mono-mp3',
106
+ } } },
107
+ })
108
+ const configMsg = `X-Timestamp:${Date()}\r\nContent-Type:application/json; charset=utf-8\r\nPath:speech.config\r\n\r\n${speechConfig}`
109
+ ws.send(configMsg, { compress: true })
110
+
111
+ const ssml = '<speak version=\'1.0\' xmlns=\'http://www.w3.org/2001/10/synthesis\' xml:lang=\'zh-CN\'>'
112
+ + `<voice name='${voice}'><prosody pitch='+0Hz' rate='+0%' volume='+0%'>${escapeXml(text)}</prosody></voice></speak>`
113
+ const ssmlMsg = `X-RequestId:${uuid()}\r\nContent-Type:application/ssml+xml\r\nX-Timestamp:${Date()}Z\r\nPath:ssml\r\n\r\n${ssml}`
114
+ ws.send(ssmlMsg, { compress: true })
115
+ })
116
+ })
117
+ }