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