149 lines
6.4 KiB
JavaScript
149 lines
6.4 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.EdgeTTS = void 0;
|
|
const node_crypto_1 = require("node:crypto");
|
|
const node_fs_1 = require("node:fs");
|
|
const ws_1 = require("ws");
|
|
const https_proxy_agent_1 = require("https-proxy-agent");
|
|
const drm_1 = require("./drm");
|
|
function escapeXml(unsafe) {
|
|
return unsafe.replace(/[<>&"']/g, (c) => {
|
|
switch (c) {
|
|
case '<': return '<';
|
|
case '>': return '>';
|
|
case '&': return '&';
|
|
case '"': return '"';
|
|
case "'": return ''';
|
|
default: return c;
|
|
}
|
|
});
|
|
}
|
|
class EdgeTTS {
|
|
constructor({ voice = 'zh-CN-XiaoyiNeural', lang = 'zh-CN', outputFormat = 'audio-24khz-48kbitrate-mono-mp3', saveSubtitles = false, proxy, rate = 'default', pitch = 'default', volume = 'default', timeout = 10000 } = {}) {
|
|
this.voice = voice;
|
|
this.lang = lang;
|
|
this.outputFormat = outputFormat;
|
|
this.saveSubtitles = saveSubtitles;
|
|
this.proxy = proxy;
|
|
this.rate = rate;
|
|
this.pitch = pitch;
|
|
this.volume = volume;
|
|
this.timeout = timeout;
|
|
}
|
|
async _connectWebSocket() {
|
|
const wsConnect = new ws_1.WebSocket(`wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1?TrustedClientToken=${drm_1.TRUSTED_CLIENT_TOKEN}&Sec-MS-GEC=${(0, drm_1.generateSecMsGecToken)()}&Sec-MS-GEC-Version=1-${drm_1.CHROMIUM_FULL_VERSION}`, {
|
|
host: 'speech.platform.bing.com',
|
|
origin: 'chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold',
|
|
headers: {
|
|
'Pragma': 'no-cache',
|
|
'Cache-Control': 'no-cache',
|
|
'User-Agent': `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${drm_1.CHROMIUM_FULL_VERSION.split('.')[0]}.0.0.0 Safari/537.36 Edg/${drm_1.CHROMIUM_FULL_VERSION.split('.')[0]}.0.0.0`,
|
|
'Accept-Encoding': 'gzip, deflate, br, zstd',
|
|
'Accept-Language': 'en-US,en;q=0.9'
|
|
},
|
|
agent: this.proxy ? new https_proxy_agent_1.HttpsProxyAgent(this.proxy) : undefined
|
|
});
|
|
return new Promise((resolve, reject) => {
|
|
wsConnect.on('open', () => {
|
|
wsConnect.send(`Content-Type:application/json; charset=utf-8\r\nPath:speech.config\r\n\r\n
|
|
{
|
|
"context": {
|
|
"synthesis": {
|
|
"audio": {
|
|
"metadataoptions": {
|
|
"sentenceBoundaryEnabled": "false",
|
|
"wordBoundaryEnabled": "true"
|
|
},
|
|
"outputFormat": "${this.outputFormat}"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`);
|
|
resolve(wsConnect);
|
|
});
|
|
wsConnect.on('error', (err) => {
|
|
reject(err);
|
|
});
|
|
});
|
|
}
|
|
_saveSubFile(subFile, text, audioPath) {
|
|
let subPath = audioPath + '.json';
|
|
let subChars = text.split('');
|
|
let subCharIndex = 0;
|
|
subFile.forEach((cue, index) => {
|
|
let fullPart = '';
|
|
let stepIndex = 0;
|
|
for (let sci = subCharIndex; sci < subChars.length; sci++) {
|
|
if (subChars[sci] === cue.part[stepIndex]) {
|
|
fullPart = fullPart + subChars[sci];
|
|
stepIndex += 1;
|
|
}
|
|
else if (subChars[sci] === subFile?.[index + 1]?.part?.[0]) {
|
|
subCharIndex = sci;
|
|
break;
|
|
}
|
|
else {
|
|
fullPart = fullPart + subChars[sci];
|
|
}
|
|
}
|
|
cue.part = fullPart;
|
|
});
|
|
(0, node_fs_1.writeFileSync)(subPath, JSON.stringify(subFile, null, ' '), { encoding: 'utf-8' });
|
|
}
|
|
async ttsPromise(text, audioPath) {
|
|
const _wsConnect = await this._connectWebSocket();
|
|
return new Promise((resolve, reject) => {
|
|
let audioStream = (0, node_fs_1.createWriteStream)(audioPath);
|
|
let subFile = [];
|
|
let timeout = setTimeout(() => reject('Timed out'), this.timeout);
|
|
_wsConnect.on('message', async (data, isBinary) => {
|
|
if (isBinary) {
|
|
let separator = 'Path:audio\r\n';
|
|
let index = data.indexOf(separator) + separator.length;
|
|
let audioData = data.subarray(index);
|
|
audioStream.write(audioData);
|
|
}
|
|
else {
|
|
let message = data.toString();
|
|
if (message.includes('Path:turn.end')) {
|
|
audioStream.end();
|
|
audioStream.on('finish', () => {
|
|
_wsConnect.close();
|
|
if (this.saveSubtitles) {
|
|
this._saveSubFile(subFile, text, audioPath);
|
|
}
|
|
clearTimeout(timeout);
|
|
resolve();
|
|
});
|
|
}
|
|
else if (message.includes('Path:audio.metadata')) {
|
|
let splitTexts = message.split('\r\n');
|
|
try {
|
|
let metadata = JSON.parse(splitTexts[splitTexts.length - 1]);
|
|
metadata['Metadata'].forEach((element) => {
|
|
subFile.push({
|
|
part: element['Data']['text']['Text'],
|
|
start: Math.floor(element['Data']['Offset'] / 10000),
|
|
end: Math.floor((element['Data']['Offset'] + element['Data']['Duration']) / 10000)
|
|
});
|
|
});
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
});
|
|
let requestId = (0, node_crypto_1.randomBytes)(16).toString('hex');
|
|
_wsConnect.send(`X-RequestId:${requestId}\r\nContent-Type:application/ssml+xml\r\nPath:ssml\r\n\r\n
|
|
` + `<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="${this.lang}">
|
|
<voice name="${this.voice}">
|
|
<prosody rate="${this.rate}" pitch="${this.pitch}" volume="${this.volume}">
|
|
${escapeXml(text)}
|
|
</prosody>
|
|
</voice>
|
|
</speak>`);
|
|
});
|
|
}
|
|
}
|
|
exports.EdgeTTS = EdgeTTS;
|