@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.
- package/README.md +85 -0
- package/cordis.patch.yml +5 -0
- package/lib/client.js +1072 -0
- package/lib/edge-tts.js +117 -0
- package/lib/index.js +1394 -0
- package/package.json +33 -0
package/lib/edge-tts.js
ADDED
|
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
37
|
+
.replace(/"/g, '"').replace(/'/g, ''')
|
|
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
|
+
}
|