@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.
@@ -23,26 +23,38 @@ var EdgeTTSModule = (() => {
23
23
  // src/browser/EdgeTTS.browser.ts
24
24
  var EdgeTTS_browser_exports = {};
25
25
  __export(EdgeTTS_browser_exports, {
26
- EdgeTTS: () => EdgeTTS
26
+ EdgeTTS: () => EdgeTTS,
27
+ EdgeTTSError: () => EdgeTTSError,
28
+ toSRT: () => toSRT,
29
+ toVTT: () => toVTT
27
30
  });
28
31
 
32
+ // src/core/crypto.ts
33
+ var impl;
34
+ function webCrypto() {
35
+ const crypto = impl ?? globalThis.crypto;
36
+ if (!crypto) throw new Error("Web Crypto is not available in this environment");
37
+ return crypto;
38
+ }
39
+
29
40
  // src/config/constants.ts
30
41
  var Constants = {
31
42
  TRUSTED_CLIENT_TOKEN: "6A5AA1D4EAFF4E9FB37E23D68491D6F4",
32
43
  BASE_URL: "https://speech.platform.bing.com/consumer/speech/synthesize/readaloud",
33
- WSS_URL: "wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1",
44
+ // NOTE: token goes as Ocp-Apim-Subscription-Key here; Edge user agents only.
45
+ WSS_URL: "wss://api.msedgeservices.com/tts/cognitiveservices/websocket/v1",
34
46
  VOICES_URL: "https://speech.platform.bing.com/consumer/speech/synthesize/readaloud/voices/list",
35
- CHROMIUM_FULL_VERSION: "143.0.3650.75",
36
- CHROMIUM_MAJOR_VERSION: "143",
37
- VERSION_MS_GEC: "1-143.0.3650",
47
+ CHROMIUM_FULL_VERSION: "155.0.0.0",
48
+ CHROMIUM_MAJOR_VERSION: "155",
49
+ VERSION_MS_GEC: "1-155.0.4283.45",
38
50
  token32() {
39
51
  const bytes = new Uint8Array(16);
40
- crypto.getRandomValues(bytes);
52
+ webCrypto().getRandomValues(bytes);
41
53
  return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("").toUpperCase();
42
54
  },
43
55
  getBaseHeaders() {
44
56
  return {
45
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0",
57
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36 Edg/155.0.0.0",
46
58
  "Accept-Encoding": "gzip, deflate, br, zstd",
47
59
  "Accept-Language": "en-US,en;q=0.9",
48
60
  "Cookie": "MUID=" + this.token32()
@@ -52,13 +64,12 @@ var EdgeTTSModule = (() => {
52
64
  "Pragma": "no-cache",
53
65
  "Cache-Control": "no-cache",
54
66
  "Origin": "chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold",
55
- "Sec-WebSocket-Protocol": "synthesize",
56
- "Sec-WebSocket-Version": "13",
57
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.3650.75 Safari/537.36 Edg/143.0.3650.75"
67
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36 Edg/155.0.0.0"
58
68
  },
59
69
  VOICE_HEADERS: {
60
- "Sec-CH-UA": '" Not;A Brand";v="99", "Microsoft Edge";v="143", "Chromium";v="143"',
70
+ "Sec-CH-UA": '"Microsoft Edge";v="155", "Chromium";v="155", "Not(A:Brand";v="24"',
61
71
  "Sec-CH-UA-Mobile": "?0",
72
+ "Sec-CH-UA-Platform": '"Windows"',
62
73
  "Accept": "*/*",
63
74
  "Sec-Fetch-Site": "none",
64
75
  "Sec-Fetch-Mode": "cors",
@@ -72,73 +83,489 @@ var EdgeTTSModule = (() => {
72
83
  }
73
84
  };
74
85
 
75
- // src/browser/EdgeTTS.browser.ts
76
- var EdgeTTS = class {
77
- constructor() {
86
+ // src/core/clock.ts
87
+ var skewMs = 0;
88
+ function now() {
89
+ return Date.now() + skewMs;
90
+ }
91
+
92
+ // src/core/errors.ts
93
+ var EdgeTTSError = class extends Error {
94
+ constructor(message, code, options = {}) {
95
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
96
+ __publicField(this, "code");
97
+ __publicField(this, "status");
98
+ this.name = "EdgeTTSError";
99
+ this.code = code;
100
+ if (options.status !== void 0) this.status = options.status;
101
+ }
102
+ };
103
+
104
+ // src/core/protocol.ts
105
+ var DEFAULT_OUTPUT_FORMAT = "audio-24khz-48kbitrate-mono-mp3";
106
+ var decoder = new TextDecoder();
107
+ function buildSynthesisUrl(base, token, secMsGec, version, connectionId) {
108
+ return `${base}?Ocp-Apim-Subscription-Key=${token}&Sec-MS-GEC=${secMsGec}&Sec-MS-GEC-Version=${version}&ConnectionId=${connectionId}`;
109
+ }
110
+ function buildConfigMessage(timestamp2, outputFormat) {
111
+ return `X-Timestamp:${timestamp2}\r
112
+ Content-Type:application/json; charset=utf-8\r
113
+ Path:speech.config\r
114
+ \r
115
+ {"context":{"synthesis":{"audio":{"metadataoptions":{"sentenceBoundaryEnabled":true,"wordBoundaryEnabled":true},"outputFormat":"${outputFormat}"}}}}`;
116
+ }
117
+ function buildSSMLMessage(requestId, timestamp2, ssml) {
118
+ return `X-RequestId:${requestId}\r
119
+ Content-Type:application/ssml+xml\r
120
+ X-Timestamp:${timestamp2}\r
121
+ Path:ssml\r
122
+ \r
123
+ ${ssml}`;
124
+ }
125
+ function escapeXML(text) {
126
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
127
+ }
128
+ function buildSSML(text, voice, pitch, rate, volume) {
129
+ 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">
130
+ <voice name="${voice}">
131
+ <prosody pitch="${pitch}" rate="${rate}" volume="${volume}">
132
+ ${escapeXML(text)}
133
+ </prosody>
134
+ </voice>
135
+ </speak>`;
136
+ }
137
+ function headerPath(head) {
138
+ return /Path:([^\r\n]*)/i.exec(head)?.[1]?.trim() ?? "";
139
+ }
140
+ function parseTextFrame(text) {
141
+ const split = text.indexOf("\r\n\r\n");
142
+ if (split === -1) return { path: headerPath(text), body: "" };
143
+ return { path: headerPath(text.slice(0, split)), body: text.slice(split + 4) };
144
+ }
145
+ function parseBinaryFrame(data) {
146
+ if (data.length < 2) return { path: "", body: new Uint8Array(0) };
147
+ const headerLength = data[0] << 8 | data[1];
148
+ return {
149
+ path: headerPath(decoder.decode(data.subarray(2, 2 + headerLength))),
150
+ body: data.slice(2 + headerLength)
151
+ };
152
+ }
153
+ function parseMetadata(body) {
154
+ let metadata;
155
+ try {
156
+ metadata = JSON.parse(body);
157
+ } catch {
158
+ return [];
159
+ }
160
+ if (!Array.isArray(metadata?.Metadata)) return [];
161
+ const boundaries = [];
162
+ for (const item of metadata.Metadata) {
163
+ if (item?.Type !== "WordBoundary" && item?.Type !== "SentenceBoundary") continue;
164
+ boundaries.push({
165
+ type: item.Type,
166
+ offset: item.Data.Offset,
167
+ duration: item.Data.Duration,
168
+ text: item.Data.text?.Text
169
+ });
170
+ }
171
+ return boundaries;
172
+ }
173
+ var OPENING_PUNCTUATION = `\xBF\xA1\xAB"'\u201C\u2018([`;
174
+ function sentenceTextRestorer(source) {
175
+ let cursor = 0;
176
+ return (text) => {
177
+ const index = text ? source.indexOf(text, cursor) : -1;
178
+ if (index === -1) return text;
179
+ let start = index;
180
+ while (start > cursor && OPENING_PUNCTUATION.includes(source[start - 1])) start--;
181
+ cursor = index + text.length;
182
+ return source.slice(start, cursor);
183
+ };
184
+ }
185
+ async function generateSecMsGec(trustedClientToken, nowMs) {
186
+ const ticks = Math.floor(nowMs / 1e3) + 11644473600;
187
+ const rounded = ticks - ticks % 300;
188
+ const windowsTicks = rounded * 1e7;
189
+ const data = new TextEncoder().encode(`${windowsTicks}${trustedClientToken}`);
190
+ const hashBuffer = await webCrypto().subtle.digest("SHA-256", data);
191
+ return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("").toUpperCase();
192
+ }
193
+ function generateUUID() {
194
+ return "xxxxxxxx-xxxx-xxxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
195
+ const r = Math.random() * 16 | 0;
196
+ const v = c === "x" ? r : r & 3 | 8;
197
+ return v.toString(16);
198
+ });
199
+ }
200
+ function fileExtension(format) {
201
+ if (format.includes("mp3")) return "mp3";
202
+ if (format.includes("opus") && format.includes("webm")) return "webm";
203
+ if (format.includes("opus") && format.includes("ogg")) return "ogg";
204
+ if (format.includes("wav") || format.includes("riff")) return "wav";
205
+ if (format.includes("pcm") && format.includes("raw")) return "pcm";
206
+ if (format.includes("alaw")) return "alaw";
207
+ if (format.includes("mulaw")) return "mulaw";
208
+ if (format.includes("truesilk")) return "silk";
209
+ if (format.includes("g722")) return "g722";
210
+ if (format.includes("amr")) return "amr";
211
+ return "audio";
212
+ }
213
+ function estimateDuration(bytes, outputFormat) {
214
+ const kbps = Number(/(\d+)kbitrate/.exec(outputFormat)?.[1] ?? 48);
215
+ return bytes * 8 / (kbps * 1e3);
216
+ }
217
+ function concatChunks(chunks) {
218
+ const result = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0));
219
+ let offset = 0;
220
+ for (const chunk of chunks) {
221
+ result.set(chunk, offset);
222
+ offset += chunk.length;
223
+ }
224
+ return result;
225
+ }
226
+ function normalizeVoices(data) {
227
+ const out = [];
228
+ for (const v of data || []) {
229
+ const short = v?.ShortName || "";
230
+ const locale = v?.Locale || "";
231
+ let base = short.replace(/^[a-z]{2}-[A-Z]{2}-/, "");
232
+ base = base.replace(/NeuralHD$/, "").replace(/Neural$/, "").trim();
233
+ const mix = `${v?.Name || ""} ${short}`;
234
+ const voiceType = v?.VoiceType || (/NeuralHD/i.test(mix) ? "NeuralHD" : "Neural");
235
+ const localeName = v?.LocaleName || (locale || null);
236
+ let display = v?.DisplayName || v?.FriendlyName || base || short;
237
+ display = display.replace(/^Microsoft\s+/i, "");
238
+ display = display.split(" - ")[0].trim();
239
+ display = display.replace(/\s*Online\s*\(Natural\)\s*/i, " ");
240
+ display = display.replace(/\s*Online\s*/i, " ");
241
+ display = display.replace(/\s+/g, " ").trim();
242
+ const tag = v?.VoiceTag && typeof v.VoiceTag === "object" ? v.VoiceTag : {};
243
+ const tailored = Array.isArray(tag.TailoredScenarios) ? tag.TailoredScenarios : Array.isArray(tag.ContentCategories) ? tag.ContentCategories : [];
244
+ const personalities = Array.isArray(tag.VoicePersonalities) ? tag.VoicePersonalities : [];
245
+ out.push({
246
+ Name: short || (v?.Name || ""),
247
+ DisplayName: display,
248
+ LocalName: display,
249
+ ShortName: short || (v?.Name || ""),
250
+ Gender: v?.Gender ?? null,
251
+ Locale: locale || null,
252
+ LocaleName: localeName,
253
+ SecondaryLocaleList: Array.isArray(v?.SecondaryLocaleList) ? v.SecondaryLocaleList : [],
254
+ VoiceType: voiceType,
255
+ VoiceTag: {
256
+ TailoredScenarios: tailored,
257
+ VoicePersonalities: personalities
258
+ },
259
+ FriendlyName: `${display} (${voiceType}) - ${localeName}`
260
+ });
261
+ }
262
+ return out;
263
+ }
264
+
265
+ // src/core/session.ts
266
+ var CONNECTING = 0;
267
+ var OPEN = 1;
268
+ function abortReason(signal) {
269
+ if (signal.reason !== void 0) return signal.reason;
270
+ const error = new Error("This operation was aborted");
271
+ error.name = "AbortError";
272
+ return error;
273
+ }
274
+ function toBytes(data) {
275
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
276
+ if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
277
+ return new Uint8Array(0);
278
+ }
279
+ function runSession(open, options) {
280
+ return new Promise((resolve, reject) => {
281
+ if (options.signal?.aborted) {
282
+ reject(abortReason(options.signal));
283
+ return;
284
+ }
285
+ const socket = open();
286
+ socket.binaryType = "arraybuffer";
287
+ let settled = false;
288
+ let opened = false;
289
+ let timer;
290
+ const finish = (error) => {
291
+ if (settled) return;
292
+ settled = true;
293
+ clearTimeout(timer);
294
+ options.signal?.removeEventListener("abort", onAbort);
295
+ if (error === void 0) {
296
+ resolve();
297
+ return;
298
+ }
299
+ if (socket.readyState === CONNECTING || socket.readyState === OPEN) socket.close();
300
+ reject(error);
301
+ };
302
+ const arm = () => {
303
+ clearTimeout(timer);
304
+ timer = setTimeout(() => {
305
+ finish(new EdgeTTSError("WebSocket inactivity timeout - no response from server", "TIMEOUT"));
306
+ }, options.timeout);
307
+ };
308
+ const onAbort = () => finish(abortReason(options.signal));
309
+ options.signal?.addEventListener("abort", onAbort, { once: true });
310
+ arm();
311
+ socket.onopen = () => {
312
+ if (settled) return;
313
+ opened = true;
314
+ arm();
315
+ socket.send(options.configMessage);
316
+ socket.send(options.ssmlMessage);
317
+ };
318
+ socket.onmessage = (event) => {
319
+ if (settled) return;
320
+ arm();
321
+ if (typeof event.data === "string") {
322
+ const frame2 = parseTextFrame(event.data);
323
+ if (frame2.path === "audio.metadata") {
324
+ for (const boundary of parseMetadata(frame2.body)) options.onBoundary(boundary);
325
+ } else if (frame2.path === "turn.end") {
326
+ socket.close();
327
+ }
328
+ return;
329
+ }
330
+ const frame = parseBinaryFrame(toBytes(event.data));
331
+ if (frame.path === "audio") options.onAudio(frame.body);
332
+ };
333
+ socket.onerror = (event) => finish(options.toError(event, opened));
334
+ socket.onclose = () => finish();
335
+ });
336
+ }
337
+
338
+ // src/core/subtitles.ts
339
+ function timestamp(ticks, separator) {
340
+ const total = Math.max(0, Math.round(ticks / 1e4));
341
+ const ms = total % 1e3;
342
+ const s = Math.floor(total / 1e3) % 60;
343
+ const m = Math.floor(total / 6e4) % 60;
344
+ const h = Math.floor(total / 36e5);
345
+ const pad = (n, width = 2) => String(n).padStart(width, "0");
346
+ return `${pad(h)}:${pad(m)}:${pad(s)}${separator}${pad(ms, 3)}`;
347
+ }
348
+ function toSRT(cues) {
349
+ return cues.map((cue, i) => `${i + 1}
350
+ ${timestamp(cue.offset, ",")} --> ${timestamp(cue.offset + cue.duration, ",")}
351
+ ${cue.text}
352
+ `).join("\n");
353
+ }
354
+ function toVTT(cues) {
355
+ const body = cues.map((cue) => `${timestamp(cue.offset, ".")} --> ${timestamp(cue.offset + cue.duration, ".")}
356
+ ${cue.text}
357
+ `).join("\n");
358
+ return `WEBVTT
359
+
360
+ ${body}`;
361
+ }
362
+
363
+ // src/core/base.ts
364
+ var DEFAULT_TIMEOUT = 3e4;
365
+ var DEFAULT_VOICES_CACHE_TTL = 10 * 6e4;
366
+ var NO_AUDIO_MESSAGE = "The service returned no audio. Voices are locale specific, so a voice cannot read text written in another script. Check that the voice matches the language of the text.";
367
+ function noAudioMessage(outputFormat) {
368
+ const supported = Object.values(Constants.OUTPUT_FORMAT);
369
+ if (supported.includes(outputFormat)) return NO_AUDIO_MESSAGE;
370
+ return `${NO_AUDIO_MESSAGE} The output format "${outputFormat}" may also be the cause: the service is known to support ${supported.join(", ")}.`;
371
+ }
372
+ function collect(text, words, sentences) {
373
+ const restore = sentenceTextRestorer(text);
374
+ return (boundary) => {
375
+ if (boundary.type === "WordBoundary") words.push(boundary);
376
+ else sentences.push({ ...boundary, text: restore(boundary.text) });
377
+ };
378
+ }
379
+ var EdgeTTSCore = class {
380
+ constructor(options = {}) {
78
381
  __publicField(this, "audio_stream", []);
79
- __publicField(this, "ws");
382
+ __publicField(this, "output_format", DEFAULT_OUTPUT_FORMAT);
383
+ __publicField(this, "word_boundaries", []);
384
+ __publicField(this, "sentence_boundaries", []);
385
+ __publicField(this, "options");
386
+ __publicField(this, "voicesCache");
387
+ this.options = { ...options };
388
+ }
389
+ async recoverHandshake() {
390
+ return false;
391
+ }
392
+ signature() {
393
+ return generateSecMsGec(Constants.TRUSTED_CLIENT_TOKEN, now());
80
394
  }
81
395
  async normalizeVoices(data) {
82
- const out = [];
83
- for (const v of data || []) {
84
- const short = v?.ShortName || "";
85
- const locale = v?.Locale || "";
86
- let base = short.replace(/^[a-z]{2}-[A-Z]{2}-/, "");
87
- base = base.replace(/NeuralHD$/, "").replace(/Neural$/, "").trim();
88
- const mix = `${v?.Name || ""} ${short}`;
89
- const voiceType = v?.VoiceType || (/NeuralHD/i.test(mix) ? "NeuralHD" : "Neural");
90
- const localeName = v?.LocaleName || (locale || null);
91
- let display = v?.DisplayName || v?.FriendlyName || base || short;
92
- display = display.replace(/^Microsoft\s+/i, "");
93
- display = display.split(" - ")[0].trim();
94
- display = display.replace(/\s*Online\s*\(Natural\)\s*/i, " ");
95
- display = display.replace(/\s*Online\s*/i, " ");
96
- display = display.replace(/\s+/g, " ").trim();
97
- const tag = v?.VoiceTag && typeof v.VoiceTag === "object" ? v.VoiceTag : {};
98
- const tailored = Array.isArray(tag.TailoredScenarios) ? tag.TailoredScenarios : Array.isArray(tag.ContentCategories) ? tag.ContentCategories : [];
99
- const personalities = Array.isArray(tag.VoicePersonalities) ? tag.VoicePersonalities : [];
100
- out.push({
101
- Name: short || (v?.Name || ""),
102
- DisplayName: display,
103
- LocalName: display,
104
- ShortName: short || (v?.Name || ""),
105
- Gender: v?.Gender ?? null,
106
- Locale: locale || null,
107
- LocaleName: localeName,
108
- SecondaryLocaleList: Array.isArray(v?.SecondaryLocaleList) ? v.SecondaryLocaleList : [],
109
- VoiceType: voiceType,
110
- VoiceTag: {
111
- TailoredScenarios: tailored,
112
- VoicePersonalities: personalities
113
- },
114
- FriendlyName: `${display} (${voiceType}) - ${localeName}`
115
- });
116
- }
117
- return out;
396
+ return normalizeVoices(data);
118
397
  }
119
398
  async getVoices() {
120
- const secMsGEC = await this.generateSecMsGec(Constants.TRUSTED_CLIENT_TOKEN);
121
- const response = await fetch(
122
- `${Constants.VOICES_URL}?TrustedClientToken=${Constants.TRUSTED_CLIENT_TOKEN}&Sec-MS-GEC=${secMsGEC}&Sec-MS-GEC-Version=${Constants.VERSION_MS_GEC}`,
123
- { headers: Constants.getBaseHeaders() }
124
- );
125
- const data = await response.json();
126
- return this.normalizeVoices(data.voices || []);
399
+ if (this.voicesCache && this.voicesCache.expires > Date.now()) {
400
+ return JSON.parse(JSON.stringify(this.voicesCache.voices));
401
+ }
402
+ const secMsGEC = await this.signature();
403
+ const url = `${this.options.voicesUrl ?? Constants.VOICES_URL}?TrustedClientToken=${Constants.TRUSTED_CLIENT_TOKEN}&Sec-MS-GEC=${secMsGEC}&Sec-MS-GEC-Version=${this.options.secMsGecVersion ?? Constants.VERSION_MS_GEC}`;
404
+ const voices = await this.normalizeVoices(await this.requestVoices(url));
405
+ const ttl = this.options.voicesCacheTtl ?? DEFAULT_VOICES_CACHE_TTL;
406
+ if (ttl > 0) {
407
+ this.voicesCache = { expires: Date.now() + ttl, voices: JSON.parse(JSON.stringify(voices)) };
408
+ }
409
+ return voices;
127
410
  }
128
411
  async getVoicesByLanguage(locale) {
129
412
  const voices = await this.getVoices();
130
- return voices.filter((voice) => voice.Locale.startsWith(locale));
413
+ return voices.filter((voice) => voice.Locale?.startsWith(locale));
131
414
  }
132
415
  async getVoicesByGender(gender) {
133
416
  const voices = await this.getVoices();
134
417
  return voices.filter((voice) => voice.Gender === gender);
135
418
  }
136
- generateUUID() {
137
- return "xxxxxxxx-xxxx-xxxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
138
- const r = Math.random() * 16 | 0;
139
- const v = c === "x" ? r : r & 3 | 8;
140
- return v.toString(16);
141
- });
419
+ async synthesize(text, voice = this.defaultVoice, options = {}) {
420
+ const audio = [];
421
+ const words = [];
422
+ const sentences = [];
423
+ this.audio_stream = audio;
424
+ this.word_boundaries = words;
425
+ this.sentence_boundaries = sentences;
426
+ this.output_format = options.outputFormat || DEFAULT_OUTPUT_FORMAT;
427
+ await this.run(text, voice, options, (chunk) => audio.push(chunk), collect(text, words, sentences));
428
+ }
429
+ async *synthesizeStream(text, voice = this.defaultVoice, options = {}) {
430
+ const audio = [];
431
+ const words = [];
432
+ const sentences = [];
433
+ this.audio_stream = audio;
434
+ this.word_boundaries = words;
435
+ this.sentence_boundaries = sentences;
436
+ this.output_format = options.outputFormat || DEFAULT_OUTPUT_FORMAT;
437
+ const controller = new AbortController();
438
+ const forwardAbort = () => controller.abort(abortReason(options.signal));
439
+ if (options.signal?.aborted) forwardAbort();
440
+ else options.signal?.addEventListener("abort", forwardAbort, { once: true });
441
+ const queue = [];
442
+ let wake = null;
443
+ let done = false;
444
+ let failure;
445
+ const notify = () => {
446
+ wake?.();
447
+ wake = null;
448
+ };
449
+ this.run(text, voice, { ...options, signal: controller.signal }, (chunk) => {
450
+ audio.push(chunk);
451
+ queue.push(chunk);
452
+ notify();
453
+ }, collect(text, words, sentences)).then(
454
+ () => {
455
+ done = true;
456
+ notify();
457
+ },
458
+ (error) => {
459
+ failure = error;
460
+ done = true;
461
+ notify();
462
+ }
463
+ );
464
+ try {
465
+ while (!done || queue.length > 0) {
466
+ if (queue.length === 0) {
467
+ await new Promise((resolve) => wake = resolve);
468
+ continue;
469
+ }
470
+ yield queue.shift();
471
+ }
472
+ if (failure !== void 0) throw failure;
473
+ } finally {
474
+ options.signal?.removeEventListener("abort", forwardAbort);
475
+ if (!done) controller.abort();
476
+ }
477
+ }
478
+ async generate(text, voice = this.defaultVoice, options = {}) {
479
+ const chunks = [];
480
+ const words = [];
481
+ const sentences = [];
482
+ await this.run(text, voice, options, (chunk) => chunks.push(chunk), collect(text, words, sentences));
483
+ const audio = concatChunks(chunks);
484
+ const outputFormat = options.outputFormat || DEFAULT_OUTPUT_FORMAT;
485
+ if (audio.length === 0) throw new EdgeTTSError(noAudioMessage(outputFormat), "NO_AUDIO");
486
+ return {
487
+ audio,
488
+ format: fileExtension(outputFormat),
489
+ outputFormat,
490
+ wordBoundaries: words,
491
+ sentenceBoundaries: sentences,
492
+ duration: estimateDuration(audio.length, outputFormat)
493
+ };
494
+ }
495
+ getWordBoundaries() {
496
+ return this.word_boundaries;
497
+ }
498
+ getSentenceBoundaries() {
499
+ return this.sentence_boundaries;
500
+ }
501
+ toSRT() {
502
+ return toSRT(this.sentence_boundaries.length > 0 ? this.sentence_boundaries : this.word_boundaries);
503
+ }
504
+ toVTT() {
505
+ return toVTT(this.sentence_boundaries.length > 0 ? this.sentence_boundaries : this.word_boundaries);
506
+ }
507
+ async run(text, voice, options, onAudio, onBoundary) {
508
+ const ssml = this.buildSSML(text, voice, options);
509
+ const outputFormat = options.outputFormat || DEFAULT_OUTPUT_FORMAT;
510
+ const attempt = async () => {
511
+ const requestId = generateUUID();
512
+ const url = buildSynthesisUrl(
513
+ this.options.wssUrl ?? Constants.WSS_URL,
514
+ Constants.TRUSTED_CLIENT_TOKEN,
515
+ await this.signature(),
516
+ this.options.secMsGecVersion ?? Constants.VERSION_MS_GEC,
517
+ requestId
518
+ );
519
+ await runSession(() => this.openSocket(url), {
520
+ configMessage: buildConfigMessage(this.timestamp(), outputFormat),
521
+ ssmlMessage: buildSSMLMessage(requestId, this.timestamp(), ssml),
522
+ timeout: options.timeout ?? this.options.timeout ?? DEFAULT_TIMEOUT,
523
+ signal: options.signal,
524
+ onAudio,
525
+ onBoundary,
526
+ toError: (event, opened) => this.socketError(event, opened)
527
+ });
528
+ };
529
+ try {
530
+ await attempt();
531
+ } catch (error) {
532
+ if (!(error instanceof EdgeTTSError && error.code === "HANDSHAKE")) throw error;
533
+ if (!await this.recoverHandshake()) throw error;
534
+ await attempt();
535
+ }
536
+ }
537
+ };
538
+
539
+ // src/browser/EdgeTTS.browser.ts
540
+ var EdgeTTS = class extends EdgeTTSCore {
541
+ constructor(options = {}) {
542
+ super(options);
543
+ __publicField(this, "defaultVoice", "en-US-JennyNeural");
544
+ __publicField(this, "ws");
545
+ }
546
+ openSocket(url) {
547
+ this.ws = new WebSocket(url);
548
+ return this.ws;
549
+ }
550
+ async requestVoices(url) {
551
+ const response = await fetch(url, { headers: Constants.getBaseHeaders() });
552
+ if (!response.ok) {
553
+ throw new EdgeTTSError(`Voice list request failed with status ${response.status}`, "VOICES", { status: response.status });
554
+ }
555
+ const data = await response.json();
556
+ return Array.isArray(data) ? data : data?.voices || [];
557
+ }
558
+ buildSSML(text, voice, options) {
559
+ const pitch = this.validatePitch(options.pitch ?? 0);
560
+ const rate = this.validateRate(options.rate ?? 0);
561
+ const volume = this.validateVolume(options.volume ?? 0);
562
+ return buildSSML(text, voice, pitch, rate, volume);
563
+ }
564
+ timestamp() {
565
+ return (/* @__PURE__ */ new Date()).toUTCString();
566
+ }
567
+ socketError(_event, opened) {
568
+ return new EdgeTTSError("WebSocket error", opened ? "NETWORK" : "HANDSHAKE");
142
569
  }
143
570
  validatePitch(pitch) {
144
571
  if (typeof pitch === "number") {
@@ -164,248 +591,13 @@ var EdgeTTSModule = (() => {
164
591
  }
165
592
  return `${volumeValue}%`;
166
593
  }
167
- escapeXML(text) {
168
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
169
- }
170
- getSSML(content, voice, options = {}) {
171
- const pitch = this.validatePitch(options.pitch ?? 0);
172
- const rate = this.validateRate(options.rate ?? 0);
173
- const volume = this.validateVolume(options.volume ?? 0);
174
- const escapedText = this.escapeXML(content);
175
- 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">
176
- <voice name="${voice}">
177
- <prosody pitch="${pitch}" rate="${rate}" volume="${volume}">
178
- ${escapedText}
179
- </prosody>
180
- </voice>
181
- </speak>`;
182
- }
183
- nowRFC1123() {
184
- return (/* @__PURE__ */ new Date()).toUTCString();
185
- }
186
- parseRFC1123(rfcStr) {
187
- return new Date(rfcStr);
188
- }
189
- buildTTSConfigMessage() {
190
- const timestamp = this.nowRFC1123();
191
- return `X-Timestamp:${timestamp}\r
192
- Content-Type:application/json; charset=utf-8\r
193
- Path:speech.config\r
194
- \r
195
- {"context":{"synthesis":{"audio":{"metadataoptions":{"sentenceBoundaryEnabled":false,"wordBoundaryEnabled":true},"outputFormat":"audio-24khz-48kbitrate-mono-mp3"}}}}`;
196
- }
197
- async synthesize(text, voice = "en-US-JennyNeural", options = {}) {
198
- const secMsGEC = await this.generateSecMsGec(Constants.TRUSTED_CLIENT_TOKEN);
199
- return new Promise((resolve, reject) => {
200
- this.audio_stream = [];
201
- const reqId = this.generateUUID();
202
- const url = `${Constants.WSS_URL}?TrustedClientToken=${Constants.TRUSTED_CLIENT_TOKEN}&Sec-MS-GEC=${secMsGEC}&Sec-MS-GEC-Version=${Constants.VERSION_MS_GEC}&ConnectionId=${reqId}`;
203
- this.ws = new WebSocket(url);
204
- const SSML_text = this.getSSML(text, voice, options);
205
- let timedOut = false;
206
- let inactivityTimeout;
207
- const resetInactivityTimeout = () => {
208
- clearTimeout(inactivityTimeout);
209
- inactivityTimeout = window.setTimeout(() => {
210
- timedOut = true;
211
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
212
- this.ws.close();
213
- }
214
- reject(new Error("WebSocket inactivity timeout - no response from server"));
215
- }, 3e4);
216
- };
217
- this.ws.onopen = () => {
218
- resetInactivityTimeout();
219
- const message = this.buildTTSConfigMessage();
220
- this.ws.send(message);
221
- const timestamp = this.nowRFC1123();
222
- const speechMessage = `X-RequestId:${reqId}\r
223
- Content-Type:application/ssml+xml\r
224
- X-Timestamp:${timestamp}\r
225
- Path:ssml\r
226
- \r
227
- ${SSML_text}`;
228
- this.ws.send(speechMessage);
229
- };
230
- this.ws.onmessage = (event) => {
231
- resetInactivityTimeout();
232
- if (typeof event.data === "string") {
233
- if (event.data.includes("Path:turn.end")) {
234
- this.ws?.close();
235
- }
236
- } else {
237
- const reader = new FileReader();
238
- reader.onload = () => {
239
- const arrayBuffer = reader.result;
240
- const uint8Array = new Uint8Array(arrayBuffer);
241
- this.processAudioData(uint8Array);
242
- };
243
- reader.readAsArrayBuffer(event.data);
244
- }
245
- };
246
- this.ws.onerror = (err) => {
247
- clearTimeout(inactivityTimeout);
248
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
249
- this.ws.close();
250
- }
251
- reject(new Error("WebSocket error"));
252
- };
253
- this.ws.onclose = () => {
254
- clearTimeout(inactivityTimeout);
255
- if (!timedOut) {
256
- resolve();
257
- }
258
- };
259
- });
260
- }
261
- async *synthesizeStream(text, voice = "en-US-JennyNeural", options = {}) {
262
- this.audio_stream = [];
263
- const reqId = this.generateUUID();
264
- const secMsGEC = await this.generateSecMsGec(Constants.TRUSTED_CLIENT_TOKEN);
265
- const url = `${Constants.WSS_URL}?TrustedClientToken=${Constants.TRUSTED_CLIENT_TOKEN}&Sec-MS-GEC=${secMsGEC}&Sec-MS-GEC-Version=${Constants.VERSION_MS_GEC}&ConnectionId=${reqId}`;
266
- this.ws = new WebSocket(url);
267
- const SSML_text = this.getSSML(text, voice, options);
268
- const queue = [];
269
- let done = false;
270
- let error = null;
271
- let notify = null;
272
- const push = (chunk) => {
273
- queue.push(chunk);
274
- if (notify) {
275
- notify();
276
- notify = null;
277
- }
278
- };
279
- let timedOut = false;
280
- let inactivityTimeout;
281
- const resetInactivityTimeout = () => {
282
- clearTimeout(inactivityTimeout);
283
- inactivityTimeout = window.setTimeout(() => {
284
- timedOut = true;
285
- error = new Error("WebSocket inactivity timeout - no response from server");
286
- done = true;
287
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
288
- this.ws.close();
289
- }
290
- if (notify) {
291
- notify();
292
- notify = null;
293
- }
294
- }, 3e4);
295
- };
296
- this.ws.onopen = () => {
297
- resetInactivityTimeout();
298
- const message = this.buildTTSConfigMessage();
299
- this.ws.send(message);
300
- const timestamp = this.nowRFC1123();
301
- const speechMessage = `X-RequestId:${reqId}\r
302
- Content-Type:application/ssml+xml\r
303
- X-Timestamp:${timestamp}\r
304
- Path:ssml\r
305
- \r
306
- ${SSML_text}`;
307
- this.ws.send(speechMessage);
308
- };
309
- this.ws.onmessage = (event) => {
310
- resetInactivityTimeout();
311
- if (typeof event.data === "string") {
312
- if (event.data.includes("Path:turn.end")) {
313
- this.ws?.close();
314
- }
315
- } else {
316
- const reader = new FileReader();
317
- reader.onload = () => {
318
- const arrayBuffer = reader.result;
319
- const uint8Array = new Uint8Array(arrayBuffer);
320
- const needle = new TextEncoder().encode("Path:audio\r\n");
321
- const audioStartIndex = this.indexOf(uint8Array, needle);
322
- if (audioStartIndex !== -1) {
323
- const audioChunk = uint8Array.slice(audioStartIndex + needle.length);
324
- this.audio_stream.push(audioChunk);
325
- push(audioChunk);
326
- }
327
- };
328
- reader.readAsArrayBuffer(event.data);
329
- }
330
- };
331
- this.ws.onerror = (err) => {
332
- clearTimeout(inactivityTimeout);
333
- error = new Error("WebSocket error");
334
- done = true;
335
- if (notify) {
336
- notify();
337
- notify = null;
338
- }
339
- };
340
- this.ws.onclose = () => {
341
- clearTimeout(inactivityTimeout);
342
- done = true;
343
- if (notify) {
344
- notify();
345
- notify = null;
346
- }
347
- };
348
- while (!done || queue.length > 0) {
349
- if (queue.length === 0) {
350
- await new Promise((resolve) => notify = resolve);
351
- continue;
352
- }
353
- const chunk = queue.shift();
354
- if (chunk) {
355
- yield chunk;
356
- }
357
- }
358
- if (error) {
359
- throw error;
360
- }
361
- }
362
- processAudioData(buffer) {
363
- const needle = new TextEncoder().encode("Path:audio\r\n");
364
- const audioStartIndex = this.indexOf(buffer, needle);
365
- if (audioStartIndex !== -1) {
366
- const audioChunk = buffer.slice(audioStartIndex + needle.length);
367
- this.audio_stream.push(audioChunk);
368
- }
369
- }
370
- indexOf(arr, subarr) {
371
- for (let i = 0; i <= arr.length - subarr.length; i++) {
372
- let match = true;
373
- for (let j = 0; j < subarr.length; j++) {
374
- if (arr[i + j] !== subarr[j]) {
375
- match = false;
376
- break;
377
- }
378
- }
379
- if (match) return i;
380
- }
381
- return -1;
382
- }
383
- async generateSecMsGec(trustedClientToken) {
384
- const now = this.nowRFC1123();
385
- const fixedDate = this.parseRFC1123(now);
386
- const ticks = Math.floor(fixedDate.getTime() / 1e3) + 11644473600;
387
- const rounded = ticks - ticks % 300;
388
- const windowsTicks = rounded * 1e7;
389
- const encoder = new TextEncoder();
390
- const data = encoder.encode(`${windowsTicks}${trustedClientToken}`);
391
- const hashBuffer = await crypto.subtle.digest("SHA-256", data);
392
- return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("").toUpperCase();
393
- }
394
594
  getAudioData() {
395
595
  if (this.audio_stream.length === 0) {
396
- throw new Error("No audio data available");
397
- }
398
- const totalLength = this.audio_stream.reduce((acc, chunk) => acc + chunk.length, 0);
399
- if (totalLength === 0) {
400
- throw new Error(
401
- "The service returned no audio. Voices are locale specific, so a voice cannot read text written in another script. Check that the voice matches the language of the text."
402
- );
596
+ throw new EdgeTTSError("No audio data available", "NO_AUDIO");
403
597
  }
404
- const result = new Uint8Array(totalLength);
405
- let offset = 0;
406
- for (const chunk of this.audio_stream) {
407
- result.set(chunk, offset);
408
- offset += chunk.length;
598
+ const result = concatChunks(this.audio_stream);
599
+ if (result.length === 0) {
600
+ throw new EdgeTTSError(noAudioMessage(this.output_format), "NO_AUDIO");
409
601
  }
410
602
  return result;
411
603
  }