@andresaya/edge-tts 1.8.3 → 1.9.1

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.
@@ -8,95 +8,53 @@ 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
- function ensureBuffer(data) {
13
- if (buffer_1.Buffer.isBuffer(data)) {
14
- return data;
15
- }
16
- if (data instanceof ArrayBuffer) {
17
- return buffer_1.Buffer.from(data);
18
- }
19
- if (Array.isArray(data)) {
20
- return buffer_1.Buffer.concat(data);
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
- if (typeof data === 'string') {
23
- return buffer_1.Buffer.from(data, 'utf-8');
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
- throw new Error(`Unsupported RawData type: ${typeof data}`);
26
- }
27
- class EdgeTTS {
28
- audio_stream = [];
29
- audio_format = 'mp3';
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
- async getVoices() {
81
- const secMsGEC = await this.generateSecMsGec(constants_1.Constants.TRUSTED_CLIENT_TOKEN);
82
- const url = `${constants_1.Constants.VOICES_URL}` +
83
- `?TrustedClientToken=${constants_1.Constants.TRUSTED_CLIENT_TOKEN}` +
84
- `&Sec-MS-GEC=${secMsGEC}` +
85
- `&Sec-MS-GEC-Version=${constants_1.Constants.VERSION_MS_GEC}`;
41
+ requestVoices(url) {
86
42
  const headers = {
87
43
  ...constants_1.Constants.getBaseHeaders(),
88
44
  ...constants_1.Constants.VOICE_HEADERS,
45
+ ...this.options.headers,
89
46
  "Accept-Encoding": "identity", // evita gzip/br/zstd
90
47
  };
91
- const voicesRaw = await new Promise((resolve, reject) => {
92
- const req = https_1.default.request(url, { method: "GET", headers }, (res) => {
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");
98
56
  if (res.statusCode !== 200) {
99
- reject(new Error(`Voice list request failed with status ${res.statusCode}`));
57
+ reject(new errors_1.EdgeTTSError(`Voice list request failed with status ${res.statusCode}`, 'VOICES', { status: res.statusCode }));
100
58
  return;
101
59
  }
102
60
  try {
@@ -108,28 +66,47 @@ class EdgeTTS {
108
66
  resolve(Array.isArray(voices) ? voices : []);
109
67
  }
110
68
  catch (e) {
111
- reject(new Error("Invalid voice list JSON: " + (e?.message || String(e))));
69
+ reject(new errors_1.EdgeTTSError("Invalid voice list JSON: " + (e?.message || String(e)), 'VOICES', { cause: e }));
112
70
  }
113
71
  });
114
72
  });
115
73
  req.on("error", reject);
116
74
  req.end();
117
75
  });
118
- return this.normalizeVoices(voicesRaw);
119
76
  }
120
- async getVoicesByLanguage(locale) {
121
- const voices = await this.getVoices();
122
- return voices.filter(voice => voice.Locale.startsWith(locale));
123
- }
124
- async getVoicesByGender(gender) {
125
- const voices = await this.getVoices();
126
- return voices.filter(voice => voice.Gender === gender);
127
- }
128
- generateUUID() {
129
- return 'xxxxxxxx-xxxx-xxxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
130
- const r = Math.random() * 16 | 0;
131
- const v = c === 'x' ? r : (r & 0x3 | 0x8);
132
- return v.toString(16);
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();
133
110
  });
134
111
  }
135
112
  validatePitch(pitch) {
@@ -137,7 +114,7 @@ class EdgeTTS {
137
114
  return (pitch >= 0 ? `+${pitch}Hz` : `${pitch}Hz`);
138
115
  }
139
116
  if (!/^[+-]?\d{1,3}(?:\.\d+)?Hz$/.test(pitch)) {
140
- throw new Error("Invalid pitch format. Expected format: '-100Hz to +100Hz' or a number.");
117
+ throw new errors_1.EdgeTTSError("Invalid pitch format. Expected format: '-100Hz to +100Hz' or a number.", 'INVALID_OPTION');
141
118
  }
142
119
  return pitch;
143
120
  }
@@ -146,7 +123,7 @@ class EdgeTTS {
146
123
  if (typeof rate === 'string') {
147
124
  rateValue = parseFloat(rate.replace('%', ''));
148
125
  if (isNaN(rateValue))
149
- throw new Error("Invalid rate format.");
126
+ throw new errors_1.EdgeTTSError("Invalid rate format.", 'INVALID_OPTION');
150
127
  }
151
128
  else {
152
129
  rateValue = rate;
@@ -161,90 +138,16 @@ class EdgeTTS {
161
138
  if (typeof volume === 'string') {
162
139
  volumeValue = parseInt(volume.replace('%', ''), 10);
163
140
  if (isNaN(volumeValue))
164
- throw new Error("Invalid volume format.");
141
+ throw new errors_1.EdgeTTSError("Invalid volume format.", 'INVALID_OPTION');
165
142
  }
166
143
  else {
167
144
  volumeValue = volume;
168
145
  }
169
146
  if (volumeValue < -100 || volumeValue > 100) {
170
- throw new Error("Invalid volume. Expected a value from -100% to 100%.");
147
+ throw new errors_1.EdgeTTSError("Invalid volume. Expected a value from -100% to 100%.", 'INVALID_OPTION');
171
148
  }
172
149
  return `${volumeValue}%`;
173
150
  }
174
- async synthesize(text, voice = 'en-US-AnaNeural', options = {}) {
175
- const secMsGEC = await this.generateSecMsGec(constants_1.Constants.TRUSTED_CLIENT_TOKEN);
176
- return new Promise((resolve, reject) => {
177
- this.audio_stream = [];
178
- this.word_boundaries = [];
179
- const reqId = this.generateUUID();
180
- const url = `${constants_1.Constants.WSS_URL}?Ocp-Apim-Subscription-Key=${constants_1.Constants.TRUSTED_CLIENT_TOKEN}&Sec-MS-GEC=${secMsGEC}&Sec-MS-GEC-Version=${constants_1.Constants.VERSION_MS_GEC}&ConnectionId=${reqId}`;
181
- this.ws = new ws_1.default(url, {
182
- headers: { ...constants_1.Constants.getBaseHeaders(), ...constants_1.Constants.WSS_HEADERS }
183
- });
184
- const SSML_text = this.getSSML(text, voice, options);
185
- const outputFormat = options.outputFormat || 'audio-24khz-48kbitrate-mono-mp3';
186
- this.output_format = outputFormat;
187
- let timedOut = false;
188
- let inactivityTimeout;
189
- const resetInactivityTimeout = () => {
190
- clearTimeout(inactivityTimeout);
191
- inactivityTimeout = setTimeout(() => {
192
- timedOut = true;
193
- if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
194
- this.ws.close();
195
- }
196
- reject(new Error("WebSocket inactivity timeout - no response from server"));
197
- }, 30000); // 30 seconds of inactivity
198
- };
199
- this.ws.on('open', () => {
200
- resetInactivityTimeout(); // start the inactivity timeout
201
- const message = this.buildTTSConfigMessage(outputFormat);
202
- this.ws.send(message);
203
- const timestamp = this.nowRFC1123();
204
- const speechMessage = `X-RequestId:${reqId}\r\nContent-Type:application/ssml+xml\r\nX-Timestamp:${timestamp}\r\nPath:ssml\r\n\r\n${SSML_text}`;
205
- this.ws.send(speechMessage);
206
- });
207
- this.ws.on('message', (data) => {
208
- resetInactivityTimeout(); // restart inactivity timeout
209
- this.processAudioData(data);
210
- });
211
- this.ws.on('error', (err) => {
212
- clearTimeout(inactivityTimeout);
213
- if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
214
- this.ws.close();
215
- }
216
- reject(err);
217
- });
218
- this.ws.on('close', () => {
219
- clearTimeout(inactivityTimeout);
220
- if (!timedOut) {
221
- resolve();
222
- }
223
- });
224
- });
225
- }
226
- escapeXML(text) {
227
- return text
228
- .replace(/&/g, '&amp;')
229
- .replace(/</g, '&lt;')
230
- .replace(/>/g, '&gt;')
231
- .replace(/"/g, '&quot;')
232
- .replace(/'/g, '&apos;');
233
- }
234
- getSSML(content, voice, options = {}) {
235
- const pitch = this.validatePitch(options.pitch ?? 0);
236
- const rate = this.validateRate(options.rate ?? 0);
237
- const volume = this.validateVolume(options.volume ?? 0);
238
- const escapedText = this.escapeXML(content);
239
- 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">
240
- <voice name="${voice}">
241
- <prosody pitch="${pitch}" rate="${rate}" volume="${volume}">
242
- ${escapedText}
243
- </prosody>
244
- </voice>
245
- </speak>
246
- `;
247
- }
248
151
  nowRFC1123(timeZone = 'UTC') {
249
152
  const now = new Date();
250
153
  const options = {
@@ -260,223 +163,28 @@ class EdgeTTS {
260
163
  };
261
164
  return now.toLocaleString('en-US', options);
262
165
  }
263
- parseRFC1123(rfcStr) {
264
- return new Date(rfcStr);
265
- }
266
- buildTTSConfigMessage(outputFormat = 'audio-24khz-48kbitrate-mono-mp3') {
267
- const timestamp = this.nowRFC1123();
268
- return `X-Timestamp:${timestamp}\r\nContent-Type:application/json; charset=utf-8\r\nPath:speech.config\r\n\r\n` +
269
- `{"context":{"synthesis":{"audio":{"metadataoptions":{"sentenceBoundaryEnabled":false,"wordBoundaryEnabled":true},"outputFormat":"${outputFormat}"}}}}`;
270
- }
271
- async *synthesizeStream(text, voice = 'en-US-AnaNeural', options = {}) {
272
- this.audio_stream = [];
273
- this.word_boundaries = [];
274
- const reqId = this.generateUUID();
275
- const secMsGEC = await this.generateSecMsGec(constants_1.Constants.TRUSTED_CLIENT_TOKEN);
276
- const url = `${constants_1.Constants.WSS_URL}?Ocp-Apim-Subscription-Key=${constants_1.Constants.TRUSTED_CLIENT_TOKEN}&Sec-MS-GEC=${secMsGEC}&Sec-MS-GEC-Version=${constants_1.Constants.VERSION_MS_GEC}&ConnectionId=${reqId}`;
277
- this.ws = new ws_1.default(url, {
278
- headers: { ...constants_1.Constants.getBaseHeaders(), ...constants_1.Constants.WSS_HEADERS }
279
- });
280
- const SSML_text = this.getSSML(text, voice, options);
281
- const outputFormat = options.outputFormat || 'audio-24khz-48kbitrate-mono-mp3';
282
- this.output_format = outputFormat;
283
- const queue = [];
284
- let done = false;
285
- let error = null;
286
- let notify = null;
287
- const push = (chunk) => {
288
- queue.push(chunk);
289
- if (notify) {
290
- notify();
291
- notify = null;
292
- }
293
- };
294
- let timedOut = false;
295
- let inactivityTimeout;
296
- const resetInactivityTimeout = () => {
297
- clearTimeout(inactivityTimeout);
298
- inactivityTimeout = setTimeout(() => {
299
- timedOut = true;
300
- error = new Error("WebSocket inactivity timeout - no response from server");
301
- done = true;
302
- if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
303
- this.ws.close();
304
- }
305
- if (notify) {
306
- notify();
307
- notify = null;
308
- }
309
- }, 30000); // 30 seconds of inactivity
310
- };
311
- this.ws.on('open', () => {
312
- resetInactivityTimeout(); // start the inactivity timeout
313
- const message = this.buildTTSConfigMessage(outputFormat);
314
- this.ws.send(message);
315
- const timestamp = this.nowRFC1123();
316
- const speechMessage = `X-RequestId:${reqId}\r\nContent-Type:application/ssml+xml\r\nX-Timestamp:${timestamp}\r\nPath:ssml\r\n\r\n${SSML_text}`;
317
- this.ws.send(speechMessage);
318
- });
319
- this.ws.on('message', (data) => {
320
- resetInactivityTimeout(); // restart inactivity timeout
321
- const buffer = ensureBuffer(data);
322
- const needle = buffer_1.Buffer.from('Path:audio\r\n');
323
- const audioStartIndex = buffer.indexOf(new Uint8Array(needle));
324
- if (audioStartIndex !== -1) {
325
- const audioChunk = buffer.subarray(audioStartIndex + needle.length);
326
- const chunk = new Uint8Array(audioChunk);
327
- this.audio_stream.push(chunk);
328
- push(chunk);
329
- }
330
- if (buffer.toString().includes("Path:audio.metadata")) {
331
- const metadataStart = buffer.indexOf("\r\n\r\n") + 4;
332
- const metadataJson = buffer.toString().substring(metadataStart);
333
- const meta = this.parseMetadata(metadataJson);
334
- if (meta !== null) {
335
- this.word_boundaries.push(meta);
336
- }
337
- return;
338
- }
339
- if (buffer.toString().includes('Path:turn.end')) {
340
- this.ws?.close();
341
- }
342
- });
343
- this.ws.on('error', (err) => {
344
- clearTimeout(inactivityTimeout);
345
- error = err;
346
- done = true;
347
- if (notify) {
348
- notify();
349
- notify = null;
350
- }
351
- });
352
- this.ws.on('close', () => {
353
- clearTimeout(inactivityTimeout);
354
- done = true;
355
- if (notify) {
356
- notify();
357
- notify = null;
358
- }
359
- });
360
- while (!done || queue.length > 0) {
361
- if (queue.length === 0) {
362
- await new Promise(resolve => (notify = resolve));
363
- continue;
364
- }
365
- const chunk = queue.shift();
366
- if (chunk) {
367
- yield chunk;
368
- }
369
- }
370
- if (error) {
371
- throw error;
372
- }
373
- }
374
- processAudioData(data) {
375
- const buffer = ensureBuffer(data);
376
- const needle = buffer_1.Buffer.from("Path:audio\r\n");
377
- const audioStartIndex = buffer.indexOf(new Uint8Array(needle));
378
- if (audioStartIndex !== -1) {
379
- const audioChunk = buffer.subarray(audioStartIndex + needle.length);
380
- this.audio_stream.push(new Uint8Array(audioChunk));
381
- }
382
- if (buffer.toString().includes("Path:audio.metadata")) {
383
- const metadataStart = buffer.indexOf("\r\n\r\n") + 4;
384
- const metadataJson = buffer.toString().substring(metadataStart);
385
- const meta = this.parseMetadata(metadataJson);
386
- if (meta !== null) {
387
- this.word_boundaries.push(meta);
388
- }
389
- return;
390
- }
391
- if (buffer.toString().includes("Path:turn.end")) {
392
- this.ws?.close();
393
- }
394
- }
395
- parseMetadata(data, offsetCompensation = 0) {
396
- let metadata;
397
- try {
398
- metadata = JSON.parse(data);
399
- }
400
- catch {
401
- return null;
402
- }
403
- if (!metadata.Metadata) {
404
- return null;
405
- }
406
- for (const metaObj of metadata.Metadata) {
407
- if (metaObj.Type === "WordBoundary") {
408
- const currentOffset = metaObj.Data.Offset + offsetCompensation;
409
- const currentDuration = metaObj.Data.Duration;
410
- return {
411
- type: "WordBoundary",
412
- offset: currentOffset,
413
- duration: currentDuration,
414
- text: metaObj.Data.text?.Text,
415
- };
416
- }
417
- }
418
- return null;
419
- }
420
- generateSecMsGec = async (trustedClientToken) => {
421
- const now = this.nowRFC1123();
422
- const fixedDate = this.parseRFC1123(now);
423
- const ticks = Math.floor(fixedDate.getTime() / 1000) + 11644473600;
424
- const rounded = ticks - (ticks % 300);
425
- const windowsTicks = rounded * 10_000_000;
426
- const encoder = new TextEncoder();
427
- const data = encoder.encode(`${windowsTicks}${trustedClientToken}`);
428
- const hashBuffer = await crypto.subtle.digest('SHA-256', data);
429
- return Array.from(new Uint8Array(hashBuffer))
430
- .map(b => b.toString(16).padStart(2, '0'))
431
- .join('')
432
- .toUpperCase();
433
- };
434
166
  getDuration() {
435
167
  if (this.audio_stream.length === 0) {
436
- throw new Error("No audio data available");
168
+ throw new errors_1.EdgeTTSError("No audio data available", 'NO_AUDIO');
437
169
  }
438
- // NOTE: MP3 is CBR; formats without a bitrate (opus) assume 48 kbit/s.
439
- const bufferSize = this.toBuffer().length;
440
- const kbps = Number(/(\d+)kbitrate/.exec(this.output_format)?.[1] ?? 48);
441
- return (bufferSize * 8) / (kbps * 1000);
442
- }
443
- getFileExtension(format) {
444
- if (format.includes('mp3'))
445
- return 'mp3';
446
- if (format.includes('opus') && format.includes('webm'))
447
- return 'webm';
448
- if (format.includes('opus') && format.includes('ogg'))
449
- return 'ogg';
450
- if (format.includes('wav') || format.includes('riff'))
451
- return 'wav';
452
- if (format.includes('pcm') && format.includes('raw'))
453
- return 'pcm';
454
- if (format.includes('alaw'))
455
- return 'alaw';
456
- if (format.includes('mulaw'))
457
- return 'mulaw';
458
- if (format.includes('truesilk'))
459
- return 'silk';
460
- if (format.includes('g722'))
461
- return 'g722';
462
- if (format.includes('amr'))
463
- return 'amr';
464
- return 'audio';
170
+ return (0, protocol_1.estimateDuration)(this.toBuffer().length, this.output_format);
465
171
  }
466
172
  getAudioInfo() {
467
173
  const buffer = this.toBuffer();
468
174
  return {
469
175
  size: buffer.length,
470
- format: this.getFileExtension(this.output_format),
176
+ format: (0, protocol_1.fileExtension)(this.output_format),
471
177
  estimatedDuration: this.getDuration()
472
178
  };
473
179
  }
474
180
  async toFile(outputPath, format) {
475
181
  if (!format) {
476
- format = this.getFileExtension(this.output_format);
182
+ format = (0, protocol_1.fileExtension)(this.output_format);
477
183
  }
478
184
  const audioBuffer = this.toBuffer();
479
- const finalPath = `${outputPath}.${format}`;
185
+ const finalPath = outputPath.toLowerCase().endsWith(`.${format.toLowerCase()}`)
186
+ ? outputPath
187
+ : `${outputPath}.${format}`;
480
188
  await (0, promises_1.writeFile)(finalPath, new Uint8Array(audioBuffer));
481
189
  return finalPath;
482
190
  }
@@ -488,16 +196,14 @@ class EdgeTTS {
488
196
  }
489
197
  toBuffer() {
490
198
  if (this.audio_stream.length === 0) {
491
- throw new Error("No audio data available. Did you run synthesize() first?");
199
+ throw new errors_1.EdgeTTSError("No audio data available. Did you run synthesize() first?", 'NO_AUDIO');
492
200
  }
493
201
  // When the service cannot speak the text it answers with a single empty
494
202
  // chunk, so the stream is not empty but the audio is. Checking the byte
495
203
  // count rather than the chunk count is what catches that.
496
204
  const buffer = buffer_1.Buffer.concat(this.audio_stream);
497
205
  if (buffer.length === 0) {
498
- throw new Error("The service returned no audio. Voices are locale specific, so a voice cannot " +
499
- "read text written in another script. Check that the voice matches the language " +
500
- "of the text.");
206
+ throw new errors_1.EdgeTTSError((0, base_1.noAudioMessage)(this.output_format), 'NO_AUDIO');
501
207
  }
502
208
  return buffer;
503
209
  }
@@ -508,8 +214,5 @@ class EdgeTTS {
508
214
  const json = JSON.stringify(this.word_boundaries, null, 4);
509
215
  await (0, promises_1.writeFile)(outputPath, json);
510
216
  }
511
- getWordBoundaries() {
512
- return this.word_boundaries;
513
- }
514
217
  }
515
218
  exports.EdgeTTS = EdgeTTS;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andresaya/edge-tts",
3
- "version": "1.8.3",
3
+ "version": "1.9.1",
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
6
  "types": "dist/index.d.ts",
@@ -22,7 +22,7 @@
22
22
  "url": "https://github.com/andresayac/edge-tts/issues"
23
23
  },
24
24
  "devDependencies": {
25
- "@types/bun": "latest",
25
+ "@types/bun": "^1.3.3",
26
26
  "@types/ws": "^8.18.1",
27
27
  "esbuild": "^0.25.12",
28
28
  "typescript": "^5.6.2"
@@ -42,6 +42,7 @@
42
42
  },
43
43
  "scripts": {
44
44
  "build": "tsc",
45
+ "test": "bun test",
45
46
  "build:bin": "bun scripts/build.ts",
46
47
  "build:browser": "bun scripts/build-browser.ts"
47
48
  }