@andresaya/edge-tts 1.8.1 → 1.9.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 +173 -43
- package/dist/browser/edge-tts.esm.js +496 -304
- package/dist/browser/edge-tts.esm.js.map +4 -4
- package/dist/browser/edge-tts.esm.min.js +20 -16
- package/dist/browser/edge-tts.esm.min.js.map +4 -4
- package/dist/browser/edge-tts.umd.js +496 -304
- package/dist/browser/edge-tts.umd.js.map +4 -4
- package/dist/browser/edge-tts.umd.min.js +20 -16
- package/dist/browser/edge-tts.umd.min.js.map +4 -4
- package/dist/cli/edge-tts.js +4 -6
- package/dist/commands/SynthesizeCommand.js +19 -3
- package/dist/commands/VoiceListCommand.js +7 -3
- package/dist/config/constants.d.ts +1 -2
- package/dist/config/constants.js +11 -10
- package/dist/core/base.d.ts +33 -0
- package/dist/core/base.js +187 -0
- package/dist/core/clock.d.ts +3 -0
- package/dist/core/clock.js +20 -0
- package/dist/core/crypto.d.ts +8 -0
- package/dist/core/crypto.js +14 -0
- package/dist/core/errors.d.ts +9 -0
- package/dist/core/errors.js +15 -0
- package/dist/core/protocol.d.ts +23 -0
- package/dist/core/protocol.js +206 -0
- package/dist/core/session.d.ts +24 -0
- package/dist/core/session.js +87 -0
- package/dist/core/subtitles.d.ts +7 -0
- package/dist/core/subtitles.js +24 -0
- package/dist/core/types.d.ts +53 -0
- package/dist/core/types.js +2 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -1
- package/dist/services/EdgeTTS.d.ts +18 -41
- package/dist/services/EdgeTTS.js +83 -376
- package/package.json +10 -15
package/dist/services/EdgeTTS.js
CHANGED
|
@@ -8,93 +8,55 @@ const ws_1 = __importDefault(require("ws"));
|
|
|
8
8
|
const constants_1 = require("../config/constants");
|
|
9
9
|
const promises_1 = require("fs/promises");
|
|
10
10
|
const buffer_1 = require("buffer");
|
|
11
|
+
const http_1 = __importDefault(require("http"));
|
|
11
12
|
const https_1 = __importDefault(require("https"));
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
const crypto_1 = require("crypto");
|
|
14
|
+
const base_1 = require("../core/base");
|
|
15
|
+
const clock_1 = require("../core/clock");
|
|
16
|
+
const crypto_2 = require("../core/crypto");
|
|
17
|
+
const errors_1 = require("../core/errors");
|
|
18
|
+
const protocol_1 = require("../core/protocol");
|
|
19
|
+
// NOTE: Node 18 has no global crypto.
|
|
20
|
+
if (!globalThis.crypto)
|
|
21
|
+
(0, crypto_2.setWebCrypto)(crypto_1.webcrypto);
|
|
22
|
+
class EdgeTTS extends base_1.EdgeTTSCore {
|
|
23
|
+
defaultVoice = 'en-US-AnaNeural';
|
|
24
|
+
ws;
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
super(options);
|
|
21
27
|
}
|
|
22
|
-
|
|
23
|
-
return
|
|
28
|
+
generateSecMsGec = async (trustedClientToken) => {
|
|
29
|
+
return (0, protocol_1.generateSecMsGec)(trustedClientToken, (0, clock_1.now)());
|
|
30
|
+
};
|
|
31
|
+
async generate(text, voice, options) {
|
|
32
|
+
const result = await super.generate(text, voice, options);
|
|
33
|
+
return { ...result, audio: buffer_1.Buffer.from(result.audio.buffer, result.audio.byteOffset, result.audio.byteLength) };
|
|
24
34
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
output_format = 'audio-24khz-48kbitrate-mono-mp3';
|
|
31
|
-
word_boundaries = [];
|
|
32
|
-
ws;
|
|
33
|
-
async normalizeVoices(data) {
|
|
34
|
-
const out = [];
|
|
35
|
-
for (const v of data || []) {
|
|
36
|
-
const short = v?.ShortName || "";
|
|
37
|
-
const locale = v?.Locale || "";
|
|
38
|
-
// base: remove locale prefix and Neural/NeuralHD suffix
|
|
39
|
-
let base = short.replace(/^[a-z]{2}-[A-Z]{2}-/, "");
|
|
40
|
-
base = base.replace(/NeuralHD$/, "").replace(/Neural$/, "").trim();
|
|
41
|
-
// VoiceType: if NeuralHD/Neural in Name or ShortName
|
|
42
|
-
const mix = `${v?.Name || ""} ${short}`;
|
|
43
|
-
const voiceType = v?.VoiceType || (/NeuralHD/i.test(mix) ? "NeuralHD" : "Neural");
|
|
44
|
-
// LocaleName: prefer LocaleName -> LanguageName -> locale
|
|
45
|
-
const localeName = v?.LocaleName || (locale || null);
|
|
46
|
-
// DisplayName: prefer DisplayName -> FriendlyName -> base -> short
|
|
47
|
-
let display = v?.DisplayName || v?.FriendlyName || base || short;
|
|
48
|
-
display = display.replace(/^Microsoft\s+/i, "");
|
|
49
|
-
display = display.split(" - ")[0].trim();
|
|
50
|
-
display = display.replace(/\s*Online\s*\(Natural\)\s*/i, " ");
|
|
51
|
-
display = display.replace(/\s*Online\s*/i, " ");
|
|
52
|
-
display = display.replace(/\s+/g, " ").trim();
|
|
53
|
-
// VoiceTag parsing
|
|
54
|
-
const tag = (v?.VoiceTag && typeof v.VoiceTag === "object") ? v.VoiceTag : {};
|
|
55
|
-
const tailored = Array.isArray(tag.TailoredScenarios)
|
|
56
|
-
? tag.TailoredScenarios
|
|
57
|
-
: (Array.isArray(tag.ContentCategories) ? tag.ContentCategories : []);
|
|
58
|
-
const personalities = Array.isArray(tag.VoicePersonalities)
|
|
59
|
-
? tag.VoicePersonalities
|
|
60
|
-
: [];
|
|
61
|
-
out.push({
|
|
62
|
-
Name: short || (v?.Name || ""),
|
|
63
|
-
DisplayName: display,
|
|
64
|
-
LocalName: display,
|
|
65
|
-
ShortName: short || (v?.Name || ""),
|
|
66
|
-
Gender: v?.Gender ?? null,
|
|
67
|
-
Locale: locale || null,
|
|
68
|
-
LocaleName: localeName,
|
|
69
|
-
SecondaryLocaleList: Array.isArray(v?.SecondaryLocaleList) ? v.SecondaryLocaleList : [],
|
|
70
|
-
VoiceType: voiceType,
|
|
71
|
-
VoiceTag: {
|
|
72
|
-
TailoredScenarios: tailored,
|
|
73
|
-
VoicePersonalities: personalities,
|
|
74
|
-
},
|
|
75
|
-
FriendlyName: `${display} (${voiceType}) - ${localeName}`,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
return out;
|
|
35
|
+
openSocket(url) {
|
|
36
|
+
this.ws = new ws_1.default(url, {
|
|
37
|
+
headers: { ...constants_1.Constants.getBaseHeaders(), ...constants_1.Constants.WSS_HEADERS, ...this.options.headers }
|
|
38
|
+
});
|
|
39
|
+
return this.ws;
|
|
79
40
|
}
|
|
80
|
-
|
|
81
|
-
const secMsGEC = await this.generateSecMsGec(constants_1.Constants.TRUSTED_CLIENT_TOKEN);
|
|
82
|
-
const httpsAgent = new https_1.default.Agent({ rejectUnauthorized: false });
|
|
83
|
-
const url = `${constants_1.Constants.VOICES_URL}` +
|
|
84
|
-
`?TrustedClientToken=${constants_1.Constants.TRUSTED_CLIENT_TOKEN}` +
|
|
85
|
-
`&Sec-MS-GEC=${secMsGEC}` +
|
|
86
|
-
`&Sec-MS-GEC-Version=${constants_1.Constants.VERSION_MS_GEC}`;
|
|
41
|
+
requestVoices(url) {
|
|
87
42
|
const headers = {
|
|
88
43
|
...constants_1.Constants.getBaseHeaders(),
|
|
44
|
+
...constants_1.Constants.VOICE_HEADERS,
|
|
45
|
+
...this.options.headers,
|
|
89
46
|
"Accept-Encoding": "identity", // evita gzip/br/zstd
|
|
90
47
|
};
|
|
91
|
-
const
|
|
92
|
-
|
|
48
|
+
const client = url.startsWith('http:') ? http_1.default : https_1.default;
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const req = client.request(url, { method: "GET", headers }, (res) => {
|
|
93
51
|
const chunks = [];
|
|
94
52
|
res.on("data", (chunk) => chunks.push(buffer_1.Buffer.isBuffer(chunk) ? chunk : buffer_1.Buffer.from(chunk)));
|
|
95
53
|
res.on("error", reject);
|
|
96
54
|
res.on("end", () => {
|
|
97
55
|
const body = buffer_1.Buffer.concat(chunks).toString("utf8");
|
|
56
|
+
if (res.statusCode !== 200) {
|
|
57
|
+
reject(new errors_1.EdgeTTSError(`Voice list request failed with status ${res.statusCode}`, 'VOICES', { status: res.statusCode }));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
98
60
|
try {
|
|
99
61
|
const parsed = JSON.parse(body);
|
|
100
62
|
// soporta array directo o { voices: [...] }
|
|
@@ -104,28 +66,47 @@ class EdgeTTS {
|
|
|
104
66
|
resolve(Array.isArray(voices) ? voices : []);
|
|
105
67
|
}
|
|
106
68
|
catch (e) {
|
|
107
|
-
reject(new
|
|
69
|
+
reject(new errors_1.EdgeTTSError("Invalid voice list JSON: " + (e?.message || String(e)), 'VOICES', { cause: e }));
|
|
108
70
|
}
|
|
109
71
|
});
|
|
110
72
|
});
|
|
111
73
|
req.on("error", reject);
|
|
112
74
|
req.end();
|
|
113
75
|
});
|
|
114
|
-
return this.normalizeVoices(voicesRaw);
|
|
115
|
-
}
|
|
116
|
-
async getVoicesByLanguage(locale) {
|
|
117
|
-
const voices = await this.getVoices();
|
|
118
|
-
return voices.filter(voice => voice.Locale.startsWith(locale));
|
|
119
|
-
}
|
|
120
|
-
async getVoicesByGender(gender) {
|
|
121
|
-
const voices = await this.getVoices();
|
|
122
|
-
return voices.filter(voice => voice.Gender === gender);
|
|
123
76
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
77
|
+
buildSSML(text, voice, options) {
|
|
78
|
+
const pitch = this.validatePitch(options.pitch ?? 0);
|
|
79
|
+
const rate = this.validateRate(options.rate ?? 0);
|
|
80
|
+
const volume = this.validateVolume(options.volume ?? 0);
|
|
81
|
+
return (0, protocol_1.buildSSML)(text, voice, pitch, rate, volume);
|
|
82
|
+
}
|
|
83
|
+
timestamp() {
|
|
84
|
+
return this.nowRFC1123();
|
|
85
|
+
}
|
|
86
|
+
socketError(event, opened) {
|
|
87
|
+
const { error, message } = event;
|
|
88
|
+
const cause = error instanceof Error ? error : new Error(message || 'WebSocket error');
|
|
89
|
+
if (opened || cause.code !== undefined)
|
|
90
|
+
return cause;
|
|
91
|
+
const status = /Unexpected server response: (\d{3})/.exec(cause.message)?.[1];
|
|
92
|
+
return new errors_1.EdgeTTSError(cause.message, 'HANDSHAKE', { status: status ? Number(status) : undefined, cause });
|
|
93
|
+
}
|
|
94
|
+
async recoverHandshake() {
|
|
95
|
+
const serverTime = await this.serverTime();
|
|
96
|
+
return serverTime !== undefined && (0, clock_1.adjustClock)(serverTime);
|
|
97
|
+
}
|
|
98
|
+
serverTime() {
|
|
99
|
+
const origin = new URL((this.options.wssUrl ?? constants_1.Constants.WSS_URL).replace(/^ws/, 'http')).origin + '/';
|
|
100
|
+
const client = origin.startsWith('http:') ? http_1.default : https_1.default;
|
|
101
|
+
return new Promise(resolve => {
|
|
102
|
+
const req = client.request(origin, { method: 'HEAD', timeout: 10_000 }, (res) => {
|
|
103
|
+
res.resume();
|
|
104
|
+
const date = Date.parse(res.headers.date ?? '');
|
|
105
|
+
resolve(Number.isNaN(date) ? undefined : date);
|
|
106
|
+
});
|
|
107
|
+
req.on('timeout', () => req.destroy());
|
|
108
|
+
req.on('error', () => resolve(undefined));
|
|
109
|
+
req.end();
|
|
129
110
|
});
|
|
130
111
|
}
|
|
131
112
|
validatePitch(pitch) {
|
|
@@ -133,7 +114,7 @@ class EdgeTTS {
|
|
|
133
114
|
return (pitch >= 0 ? `+${pitch}Hz` : `${pitch}Hz`);
|
|
134
115
|
}
|
|
135
116
|
if (!/^[+-]?\d{1,3}(?:\.\d+)?Hz$/.test(pitch)) {
|
|
136
|
-
throw new
|
|
117
|
+
throw new errors_1.EdgeTTSError("Invalid pitch format. Expected format: '-100Hz to +100Hz' or a number.", 'INVALID_OPTION');
|
|
137
118
|
}
|
|
138
119
|
return pitch;
|
|
139
120
|
}
|
|
@@ -142,7 +123,7 @@ class EdgeTTS {
|
|
|
142
123
|
if (typeof rate === 'string') {
|
|
143
124
|
rateValue = parseFloat(rate.replace('%', ''));
|
|
144
125
|
if (isNaN(rateValue))
|
|
145
|
-
throw new
|
|
126
|
+
throw new errors_1.EdgeTTSError("Invalid rate format.", 'INVALID_OPTION');
|
|
146
127
|
}
|
|
147
128
|
else {
|
|
148
129
|
rateValue = rate;
|
|
@@ -157,90 +138,16 @@ class EdgeTTS {
|
|
|
157
138
|
if (typeof volume === 'string') {
|
|
158
139
|
volumeValue = parseInt(volume.replace('%', ''), 10);
|
|
159
140
|
if (isNaN(volumeValue))
|
|
160
|
-
throw new
|
|
141
|
+
throw new errors_1.EdgeTTSError("Invalid volume format.", 'INVALID_OPTION');
|
|
161
142
|
}
|
|
162
143
|
else {
|
|
163
144
|
volumeValue = volume;
|
|
164
145
|
}
|
|
165
146
|
if (volumeValue < -100 || volumeValue > 100) {
|
|
166
|
-
throw new
|
|
147
|
+
throw new errors_1.EdgeTTSError("Invalid volume. Expected a value from -100% to 100%.", 'INVALID_OPTION');
|
|
167
148
|
}
|
|
168
149
|
return `${volumeValue}%`;
|
|
169
150
|
}
|
|
170
|
-
async synthesize(text, voice = 'en-US-AnaNeural', options = {}) {
|
|
171
|
-
const secMsGEC = await this.generateSecMsGec(constants_1.Constants.TRUSTED_CLIENT_TOKEN);
|
|
172
|
-
return new Promise((resolve, reject) => {
|
|
173
|
-
this.audio_stream = [];
|
|
174
|
-
const reqId = this.generateUUID();
|
|
175
|
-
const url = `${constants_1.Constants.WSS_URL}?TrustedClientToken=${constants_1.Constants.TRUSTED_CLIENT_TOKEN}&Sec-MS-GEC=${secMsGEC}&Sec-MS-GEC-Version=${constants_1.Constants.VERSION_MS_GEC}&ConnectionId=${reqId}`;
|
|
176
|
-
this.ws = new ws_1.default(url, {
|
|
177
|
-
headers: constants_1.Constants.getBaseHeaders(),
|
|
178
|
-
rejectUnauthorized: false
|
|
179
|
-
});
|
|
180
|
-
const SSML_text = this.getSSML(text, voice, options);
|
|
181
|
-
const outputFormat = options.outputFormat || 'audio-24khz-48kbitrate-mono-mp3';
|
|
182
|
-
this.output_format = outputFormat;
|
|
183
|
-
let timedOut = false;
|
|
184
|
-
let inactivityTimeout;
|
|
185
|
-
const resetInactivityTimeout = () => {
|
|
186
|
-
clearTimeout(inactivityTimeout);
|
|
187
|
-
inactivityTimeout = setTimeout(() => {
|
|
188
|
-
timedOut = true;
|
|
189
|
-
if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
|
|
190
|
-
this.ws.close();
|
|
191
|
-
}
|
|
192
|
-
reject(new Error("WebSocket inactivity timeout - no response from server"));
|
|
193
|
-
}, 30000); // 30 seconds of inactivity
|
|
194
|
-
};
|
|
195
|
-
this.ws.on('open', () => {
|
|
196
|
-
resetInactivityTimeout(); // start the inactivity timeout
|
|
197
|
-
const message = this.buildTTSConfigMessage(outputFormat);
|
|
198
|
-
this.ws.send(message);
|
|
199
|
-
const timestamp = this.nowRFC1123();
|
|
200
|
-
const speechMessage = `X-RequestId:${reqId}\r\nContent-Type:application/ssml+xml\r\nX-Timestamp:${timestamp}\r\nPath:ssml\r\n\r\n${SSML_text}`;
|
|
201
|
-
this.ws.send(speechMessage);
|
|
202
|
-
});
|
|
203
|
-
this.ws.on('message', (data) => {
|
|
204
|
-
resetInactivityTimeout(); // restart inactivity timeout
|
|
205
|
-
this.processAudioData(data);
|
|
206
|
-
});
|
|
207
|
-
this.ws.on('error', (err) => {
|
|
208
|
-
clearTimeout(inactivityTimeout);
|
|
209
|
-
if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
|
|
210
|
-
this.ws.close();
|
|
211
|
-
}
|
|
212
|
-
reject(err);
|
|
213
|
-
});
|
|
214
|
-
this.ws.on('close', () => {
|
|
215
|
-
clearTimeout(inactivityTimeout);
|
|
216
|
-
if (!timedOut) {
|
|
217
|
-
resolve();
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
escapeXML(text) {
|
|
223
|
-
return text
|
|
224
|
-
.replace(/&/g, '&')
|
|
225
|
-
.replace(/</g, '<')
|
|
226
|
-
.replace(/>/g, '>')
|
|
227
|
-
.replace(/"/g, '"')
|
|
228
|
-
.replace(/'/g, ''');
|
|
229
|
-
}
|
|
230
|
-
getSSML(content, voice, options = {}) {
|
|
231
|
-
const pitch = this.validatePitch(options.pitch ?? 0);
|
|
232
|
-
const rate = this.validateRate(options.rate ?? 0);
|
|
233
|
-
const volume = this.validateVolume(options.volume ?? 0);
|
|
234
|
-
const escapedText = this.escapeXML(content);
|
|
235
|
-
return `<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="en-US">
|
|
236
|
-
<voice name="${voice}">
|
|
237
|
-
<prosody pitch="${pitch}" rate="${rate}" volume="${volume}">
|
|
238
|
-
${escapedText}
|
|
239
|
-
</prosody>
|
|
240
|
-
</voice>
|
|
241
|
-
</speak>
|
|
242
|
-
`;
|
|
243
|
-
}
|
|
244
151
|
nowRFC1123(timeZone = 'UTC') {
|
|
245
152
|
const now = new Date();
|
|
246
153
|
const options = {
|
|
@@ -256,223 +163,28 @@ class EdgeTTS {
|
|
|
256
163
|
};
|
|
257
164
|
return now.toLocaleString('en-US', options);
|
|
258
165
|
}
|
|
259
|
-
parseRFC1123(rfcStr) {
|
|
260
|
-
return new Date(rfcStr);
|
|
261
|
-
}
|
|
262
|
-
buildTTSConfigMessage(outputFormat = 'audio-24khz-48kbitrate-mono-mp3') {
|
|
263
|
-
const timestamp = this.nowRFC1123();
|
|
264
|
-
return `X-Timestamp:${timestamp}\r\nContent-Type:application/json; charset=utf-8\r\nPath:speech.config\r\n\r\n` +
|
|
265
|
-
`{"context":{"synthesis":{"audio":{"metadataoptions":{"sentenceBoundaryEnabled":false,"wordBoundaryEnabled":true},"outputFormat":"${outputFormat}"}}}}`;
|
|
266
|
-
}
|
|
267
|
-
async *synthesizeStream(text, voice = 'en-US-AnaNeural', options = {}) {
|
|
268
|
-
this.audio_stream = [];
|
|
269
|
-
const reqId = this.generateUUID();
|
|
270
|
-
const secMsGEC = await this.generateSecMsGec(constants_1.Constants.TRUSTED_CLIENT_TOKEN);
|
|
271
|
-
const url = `${constants_1.Constants.WSS_URL}?TrustedClientToken=${constants_1.Constants.TRUSTED_CLIENT_TOKEN}&Sec-MS-GEC=${secMsGEC}&Sec-MS-GEC-Version=${constants_1.Constants.VERSION_MS_GEC}&ConnectionId=${reqId}`;
|
|
272
|
-
this.ws = new ws_1.default(url, {
|
|
273
|
-
headers: constants_1.Constants.getBaseHeaders(),
|
|
274
|
-
rejectUnauthorized: false
|
|
275
|
-
});
|
|
276
|
-
const SSML_text = this.getSSML(text, voice, options);
|
|
277
|
-
const outputFormat = options.outputFormat || 'audio-24khz-48kbitrate-mono-mp3';
|
|
278
|
-
this.output_format = outputFormat;
|
|
279
|
-
const queue = [];
|
|
280
|
-
let done = false;
|
|
281
|
-
let error = null;
|
|
282
|
-
let notify = null;
|
|
283
|
-
const push = (chunk) => {
|
|
284
|
-
queue.push(chunk);
|
|
285
|
-
if (notify) {
|
|
286
|
-
notify();
|
|
287
|
-
notify = null;
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
let timedOut = false;
|
|
291
|
-
let inactivityTimeout;
|
|
292
|
-
const resetInactivityTimeout = () => {
|
|
293
|
-
clearTimeout(inactivityTimeout);
|
|
294
|
-
inactivityTimeout = setTimeout(() => {
|
|
295
|
-
timedOut = true;
|
|
296
|
-
error = new Error("WebSocket inactivity timeout - no response from server");
|
|
297
|
-
done = true;
|
|
298
|
-
if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
|
|
299
|
-
this.ws.close();
|
|
300
|
-
}
|
|
301
|
-
if (notify) {
|
|
302
|
-
notify();
|
|
303
|
-
notify = null;
|
|
304
|
-
}
|
|
305
|
-
}, 30000); // 30 seconds of inactivity
|
|
306
|
-
};
|
|
307
|
-
this.ws.on('open', () => {
|
|
308
|
-
resetInactivityTimeout(); // start the inactivity timeout
|
|
309
|
-
const message = this.buildTTSConfigMessage(outputFormat);
|
|
310
|
-
this.ws.send(message);
|
|
311
|
-
const timestamp = this.nowRFC1123();
|
|
312
|
-
const speechMessage = `X-RequestId:${reqId}\r\nContent-Type:application/ssml+xml\r\nX-Timestamp:${timestamp}\r\nPath:ssml\r\n\r\n${SSML_text}`;
|
|
313
|
-
this.ws.send(speechMessage);
|
|
314
|
-
});
|
|
315
|
-
this.ws.on('message', (data) => {
|
|
316
|
-
resetInactivityTimeout(); // restart inactivity timeout
|
|
317
|
-
const buffer = ensureBuffer(data);
|
|
318
|
-
const needle = buffer_1.Buffer.from('Path:audio\r\n');
|
|
319
|
-
const audioStartIndex = buffer.indexOf(new Uint8Array(needle));
|
|
320
|
-
if (audioStartIndex !== -1) {
|
|
321
|
-
const audioChunk = buffer.subarray(audioStartIndex + needle.length);
|
|
322
|
-
const chunk = new Uint8Array(audioChunk);
|
|
323
|
-
this.audio_stream.push(chunk);
|
|
324
|
-
push(chunk);
|
|
325
|
-
}
|
|
326
|
-
if (buffer.toString().includes("Path:audio.metadata")) {
|
|
327
|
-
const metadataStart = buffer.indexOf("\r\n\r\n") + 4;
|
|
328
|
-
const metadataJson = buffer.toString().substring(metadataStart);
|
|
329
|
-
const meta = this.parseMetadata(metadataJson);
|
|
330
|
-
if (meta !== null) {
|
|
331
|
-
this.word_boundaries.push(meta);
|
|
332
|
-
}
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
if (buffer.toString().includes('Path:turn.end')) {
|
|
336
|
-
this.ws?.close();
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
this.ws.on('error', (err) => {
|
|
340
|
-
clearTimeout(inactivityTimeout);
|
|
341
|
-
error = err;
|
|
342
|
-
done = true;
|
|
343
|
-
if (notify) {
|
|
344
|
-
notify();
|
|
345
|
-
notify = null;
|
|
346
|
-
}
|
|
347
|
-
});
|
|
348
|
-
this.ws.on('close', () => {
|
|
349
|
-
clearTimeout(inactivityTimeout);
|
|
350
|
-
done = true;
|
|
351
|
-
if (notify) {
|
|
352
|
-
notify();
|
|
353
|
-
notify = null;
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
while (!done || queue.length > 0) {
|
|
357
|
-
if (queue.length === 0) {
|
|
358
|
-
await new Promise(resolve => (notify = resolve));
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
const chunk = queue.shift();
|
|
362
|
-
if (chunk) {
|
|
363
|
-
yield chunk;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
if (error) {
|
|
367
|
-
throw error;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
processAudioData(data) {
|
|
371
|
-
const buffer = ensureBuffer(data);
|
|
372
|
-
const needle = buffer_1.Buffer.from("Path:audio\r\n");
|
|
373
|
-
const audioStartIndex = buffer.indexOf(new Uint8Array(needle));
|
|
374
|
-
if (audioStartIndex !== -1) {
|
|
375
|
-
const audioChunk = buffer.subarray(audioStartIndex + needle.length);
|
|
376
|
-
this.audio_stream.push(new Uint8Array(audioChunk));
|
|
377
|
-
}
|
|
378
|
-
if (buffer.toString().includes("Path:audio.metadata")) {
|
|
379
|
-
const metadataStart = buffer.indexOf("\r\n\r\n") + 4;
|
|
380
|
-
const metadataJson = buffer.toString().substring(metadataStart);
|
|
381
|
-
const meta = this.parseMetadata(metadataJson);
|
|
382
|
-
if (meta !== null) {
|
|
383
|
-
this.word_boundaries.push(meta);
|
|
384
|
-
}
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
if (buffer.toString().includes("Path:turn.end")) {
|
|
388
|
-
this.ws?.close();
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
parseMetadata(data, offsetCompensation = 0) {
|
|
392
|
-
let metadata;
|
|
393
|
-
try {
|
|
394
|
-
metadata = JSON.parse(data);
|
|
395
|
-
}
|
|
396
|
-
catch {
|
|
397
|
-
return null;
|
|
398
|
-
}
|
|
399
|
-
if (!metadata.Metadata) {
|
|
400
|
-
return null;
|
|
401
|
-
}
|
|
402
|
-
for (const metaObj of metadata.Metadata) {
|
|
403
|
-
if (metaObj.Type === "WordBoundary") {
|
|
404
|
-
const currentOffset = metaObj.Data.Offset + offsetCompensation;
|
|
405
|
-
const currentDuration = metaObj.Data.Duration;
|
|
406
|
-
return {
|
|
407
|
-
type: "WordBoundary",
|
|
408
|
-
offset: currentOffset,
|
|
409
|
-
duration: currentDuration,
|
|
410
|
-
text: metaObj.Data.text?.Text,
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
return null;
|
|
415
|
-
}
|
|
416
|
-
generateSecMsGec = async (trustedClientToken) => {
|
|
417
|
-
const now = this.nowRFC1123();
|
|
418
|
-
const fixedDate = this.parseRFC1123(now);
|
|
419
|
-
const ticks = Math.floor(fixedDate.getTime() / 1000) + 11644473600;
|
|
420
|
-
const rounded = ticks - (ticks % 300);
|
|
421
|
-
const windowsTicks = rounded * 10_000_000;
|
|
422
|
-
const encoder = new TextEncoder();
|
|
423
|
-
const data = encoder.encode(`${windowsTicks}${trustedClientToken}`);
|
|
424
|
-
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
425
|
-
return Array.from(new Uint8Array(hashBuffer))
|
|
426
|
-
.map(b => b.toString(16).padStart(2, '0'))
|
|
427
|
-
.join('')
|
|
428
|
-
.toUpperCase();
|
|
429
|
-
};
|
|
430
166
|
getDuration() {
|
|
431
167
|
if (this.audio_stream.length === 0) {
|
|
432
|
-
throw new
|
|
168
|
+
throw new errors_1.EdgeTTSError("No audio data available", 'NO_AUDIO');
|
|
433
169
|
}
|
|
434
|
-
|
|
435
|
-
const bufferSize = this.toBuffer().length;
|
|
436
|
-
const estimatedDuration = bufferSize / (24000 * 3); // 24000 Hz sample rate, 3 bytes per sample (16-bit stereo)
|
|
437
|
-
return estimatedDuration;
|
|
438
|
-
}
|
|
439
|
-
getFileExtension(format) {
|
|
440
|
-
if (format.includes('mp3'))
|
|
441
|
-
return 'mp3';
|
|
442
|
-
if (format.includes('opus') && format.includes('webm'))
|
|
443
|
-
return 'webm';
|
|
444
|
-
if (format.includes('opus') && format.includes('ogg'))
|
|
445
|
-
return 'ogg';
|
|
446
|
-
if (format.includes('wav') || format.includes('riff'))
|
|
447
|
-
return 'wav';
|
|
448
|
-
if (format.includes('pcm') && format.includes('raw'))
|
|
449
|
-
return 'pcm';
|
|
450
|
-
if (format.includes('alaw'))
|
|
451
|
-
return 'alaw';
|
|
452
|
-
if (format.includes('mulaw'))
|
|
453
|
-
return 'mulaw';
|
|
454
|
-
if (format.includes('truesilk'))
|
|
455
|
-
return 'silk';
|
|
456
|
-
if (format.includes('g722'))
|
|
457
|
-
return 'g722';
|
|
458
|
-
if (format.includes('amr'))
|
|
459
|
-
return 'amr';
|
|
460
|
-
return 'audio';
|
|
170
|
+
return (0, protocol_1.estimateDuration)(this.toBuffer().length, this.output_format);
|
|
461
171
|
}
|
|
462
172
|
getAudioInfo() {
|
|
463
173
|
const buffer = this.toBuffer();
|
|
464
174
|
return {
|
|
465
175
|
size: buffer.length,
|
|
466
|
-
format:
|
|
176
|
+
format: (0, protocol_1.fileExtension)(this.output_format),
|
|
467
177
|
estimatedDuration: this.getDuration()
|
|
468
178
|
};
|
|
469
179
|
}
|
|
470
180
|
async toFile(outputPath, format) {
|
|
471
181
|
if (!format) {
|
|
472
|
-
format =
|
|
182
|
+
format = (0, protocol_1.fileExtension)(this.output_format);
|
|
473
183
|
}
|
|
474
184
|
const audioBuffer = this.toBuffer();
|
|
475
|
-
const finalPath =
|
|
185
|
+
const finalPath = outputPath.toLowerCase().endsWith(`.${format.toLowerCase()}`)
|
|
186
|
+
? outputPath
|
|
187
|
+
: `${outputPath}.${format}`;
|
|
476
188
|
await (0, promises_1.writeFile)(finalPath, new Uint8Array(audioBuffer));
|
|
477
189
|
return finalPath;
|
|
478
190
|
}
|
|
@@ -484,16 +196,14 @@ class EdgeTTS {
|
|
|
484
196
|
}
|
|
485
197
|
toBuffer() {
|
|
486
198
|
if (this.audio_stream.length === 0) {
|
|
487
|
-
throw new
|
|
199
|
+
throw new errors_1.EdgeTTSError("No audio data available. Did you run synthesize() first?", 'NO_AUDIO');
|
|
488
200
|
}
|
|
489
201
|
// When the service cannot speak the text it answers with a single empty
|
|
490
202
|
// chunk, so the stream is not empty but the audio is. Checking the byte
|
|
491
203
|
// count rather than the chunk count is what catches that.
|
|
492
204
|
const buffer = buffer_1.Buffer.concat(this.audio_stream);
|
|
493
205
|
if (buffer.length === 0) {
|
|
494
|
-
throw new
|
|
495
|
-
"read text written in another script. Check that the voice matches the language " +
|
|
496
|
-
"of the text.");
|
|
206
|
+
throw new errors_1.EdgeTTSError((0, base_1.noAudioMessage)(this.output_format), 'NO_AUDIO');
|
|
497
207
|
}
|
|
498
208
|
return buffer;
|
|
499
209
|
}
|
|
@@ -504,8 +214,5 @@ class EdgeTTS {
|
|
|
504
214
|
const json = JSON.stringify(this.word_boundaries, null, 4);
|
|
505
215
|
await (0, promises_1.writeFile)(outputPath, json);
|
|
506
216
|
}
|
|
507
|
-
getWordBoundaries() {
|
|
508
|
-
return this.word_boundaries;
|
|
509
|
-
}
|
|
510
217
|
}
|
|
511
218
|
exports.EdgeTTS = EdgeTTS;
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andresaya/edge-tts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Edge TTS is a package that allows access to the online text-to-speech service used by Microsoft Edge without the need for Microsoft Edge, Windows, or an API key.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
6
7
|
"browser": "dist/browser/edge-tts.esm.js",
|
|
7
8
|
"unpkg": "dist/browser/edge-tts.umd.min.js",
|
|
8
9
|
"jsdelivr": "dist/browser/edge-tts.umd.min.js",
|
|
@@ -20,28 +21,16 @@
|
|
|
20
21
|
"bugs": {
|
|
21
22
|
"url": "https://github.com/andresayac/edge-tts/issues"
|
|
22
23
|
},
|
|
23
|
-
"scripts": {
|
|
24
|
-
"build": "tsc",
|
|
25
|
-
"build:bin": "bun scripts/build.ts",
|
|
26
|
-
"build:browser": "bun scripts/build-browser.ts",
|
|
27
|
-
"prepublishOnly": "npm run build && npm run build:bin && npm run build:browser"
|
|
28
|
-
},
|
|
29
24
|
"devDependencies": {
|
|
30
|
-
"@types/bun": "
|
|
25
|
+
"@types/bun": "^1.3.3",
|
|
31
26
|
"@types/ws": "^8.18.1",
|
|
32
27
|
"esbuild": "^0.25.12",
|
|
33
28
|
"typescript": "^5.6.2"
|
|
34
29
|
},
|
|
35
30
|
"dependencies": {
|
|
36
|
-
"@types/uuid": "^10.0.0",
|
|
37
31
|
"commander": "^12.1.0",
|
|
38
|
-
"events": "^3.3.0",
|
|
39
|
-
"fs": "^0.0.1-security",
|
|
40
32
|
"ws": "^8.18.3"
|
|
41
33
|
},
|
|
42
|
-
"peerDependencies": {
|
|
43
|
-
"typescript": "^5.0.0"
|
|
44
|
-
},
|
|
45
34
|
"files": [
|
|
46
35
|
"dist"
|
|
47
36
|
],
|
|
@@ -50,5 +39,11 @@
|
|
|
50
39
|
},
|
|
51
40
|
"engines": {
|
|
52
41
|
"bun": ">=0.5.0"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc",
|
|
45
|
+
"test": "bun test",
|
|
46
|
+
"build:bin": "bun scripts/build.ts",
|
|
47
|
+
"build:browser": "bun scripts/build-browser.ts"
|
|
53
48
|
}
|
|
54
|
-
}
|
|
49
|
+
}
|