@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.
@@ -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
- 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 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 voicesRaw = await new Promise((resolve, reject) => {
92
- const req = https_1.default.request(url, { method: "GET", headers, agent: httpsAgent }, (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");
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 Error("JSON inválido: " + (e?.message || String(e))));
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
- generateUUID() {
125
- return 'xxxxxxxx-xxxx-xxxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
126
- const r = Math.random() * 16 | 0;
127
- const v = c === 'x' ? r : (r & 0x3 | 0x8);
128
- 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();
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 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');
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 Error("Invalid rate format.");
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 Error("Invalid volume format.");
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 Error("Volume cannot be negative. Expected a value from -100% to 100% (or more).");
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, '&amp;')
225
- .replace(/</g, '&lt;')
226
- .replace(/>/g, '&gt;')
227
- .replace(/"/g, '&quot;')
228
- .replace(/'/g, '&apos;');
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 Error("No audio data available");
168
+ throw new errors_1.EdgeTTSError("No audio data available", 'NO_AUDIO');
433
169
  }
434
- // Estimate duration based on the size of the audio stream
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: this.getFileExtension(this.output_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 = this.getFileExtension(this.output_format);
182
+ format = (0, protocol_1.fileExtension)(this.output_format);
473
183
  }
474
184
  const audioBuffer = this.toBuffer();
475
- const finalPath = `${outputPath}.${format}`;
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 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');
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 Error("The service returned no audio. Voices are locale specific, so a voice cannot " +
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.8.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": "latest",
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
+ }