@ssml-builder-js/azure-tts-client 2.14.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/index.d.mts +161 -18
- package/dist/index.d.ts +161 -18
- package/dist/index.js +754 -174
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +749 -173
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +23 -5
- package/src/errors.ts +85 -0
- package/src/index.ts +25 -2
- package/src/outputFormats.ts +14 -3
- package/src/safe.ts +330 -81
- package/src/synthesis.ts +555 -97
- package/src/types.ts +48 -1
- package/test/synthesis.test.ts +64 -0
- package/test/v213-pipeline.test.ts +23 -16
- package/test/v214-pipeline.test.ts +7 -3
- package/test/v215-pipeline.test.ts +104 -0
- package/test/v216-pipeline.test.ts +110 -0
package/dist/index.mjs
CHANGED
|
@@ -10,6 +10,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
|
|
|
10
10
|
var AzureTtsError = class extends Error {
|
|
11
11
|
constructor(status, statusText, responseBody, requestId) {
|
|
12
12
|
super(`Azure TTS request failed: ${status} ${statusText}`);
|
|
13
|
+
this.kind = "azure-api-error";
|
|
13
14
|
this.name = "AzureTtsError";
|
|
14
15
|
this.status = status;
|
|
15
16
|
this.statusText = statusText;
|
|
@@ -25,13 +26,52 @@ var AzureTtsSdkError = class extends AzureTtsError {
|
|
|
25
26
|
this.errorDetails = errorDetails;
|
|
26
27
|
}
|
|
27
28
|
};
|
|
29
|
+
var SynthesisCancelledError = class extends Error {
|
|
30
|
+
constructor(message = "Speech synthesis was cancelled.") {
|
|
31
|
+
super(message);
|
|
32
|
+
this.kind = "cancelled";
|
|
33
|
+
this.name = "SynthesisCancelledError";
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var SynthesisTimeoutError = class extends Error {
|
|
37
|
+
constructor(message) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.kind = "timeout";
|
|
40
|
+
this.name = "SynthesisTimeoutError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var MergeError = class extends Error {
|
|
44
|
+
constructor(message, cause) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.kind = "merge-error";
|
|
47
|
+
this.name = "MergeError";
|
|
48
|
+
this.cause = cause;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var AudioFormatMismatchError = class extends Error {
|
|
52
|
+
constructor(message, inputSpecs = []) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.kind = "audio-format-mismatch";
|
|
55
|
+
this.name = "AudioFormatMismatchError";
|
|
56
|
+
this.inputSpecs = inputSpecs;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
28
59
|
var UnsupportedMergeFormatError = class extends Error {
|
|
29
60
|
constructor(format) {
|
|
30
61
|
super(`Audio format "${format}" cannot be safely concatenated; container re-multiplexing is required.`);
|
|
62
|
+
this.kind = "unsupported-format-error";
|
|
31
63
|
this.name = "UnsupportedMergeFormatError";
|
|
32
64
|
this.format = format;
|
|
33
65
|
}
|
|
34
66
|
};
|
|
67
|
+
function toSynthesisError(error) {
|
|
68
|
+
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof AudioFormatMismatchError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
|
|
69
|
+
return error;
|
|
70
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
71
|
+
if (/cancel|abort/i.test(message)) return new SynthesisCancelledError(message);
|
|
72
|
+
if (/tim(?:e|ed) ?out/i.test(message)) return new SynthesisTimeoutError(message);
|
|
73
|
+
return createSpeechSdkError(error);
|
|
74
|
+
}
|
|
35
75
|
function createSpeechSdkError(error) {
|
|
36
76
|
const message = error instanceof Error ? error.message : String(error);
|
|
37
77
|
return new AzureTtsSdkError(message);
|
|
@@ -39,9 +79,7 @@ function createSpeechSdkError(error) {
|
|
|
39
79
|
|
|
40
80
|
// src/synthesis.ts
|
|
41
81
|
import * as SpeechSDK2 from "microsoft-cognitiveservices-speech-sdk";
|
|
42
|
-
|
|
43
|
-
// src/speechConfig.ts
|
|
44
|
-
import { SpeechConfig } from "microsoft-cognitiveservices-speech-sdk";
|
|
82
|
+
import { getSsmlSourceMap } from "@ssml-builder-js/ssml-core";
|
|
45
83
|
|
|
46
84
|
// src/outputFormats.ts
|
|
47
85
|
import * as SpeechSDK from "microsoft-cognitiveservices-speech-sdk";
|
|
@@ -87,6 +125,14 @@ var OUTPUT_FORMATS = {
|
|
|
87
125
|
"amr-wb-16000hz": SpeechSDK.SpeechSynthesisOutputFormat.AmrWb16000Hz,
|
|
88
126
|
"g722-16khz-64kbps": SpeechSDK.SpeechSynthesisOutputFormat.G72216Khz64Kbps
|
|
89
127
|
};
|
|
128
|
+
function resolveMimeType(outputFormat) {
|
|
129
|
+
if (/(?:wav|wave|riff)/i.test(outputFormat)) return "audio/wav";
|
|
130
|
+
if (/(?:mp3|mpeg)/i.test(outputFormat)) return "audio/mpeg";
|
|
131
|
+
if (/ogg/i.test(outputFormat)) return "audio/ogg";
|
|
132
|
+
if (/webm/i.test(outputFormat)) return "audio/webm";
|
|
133
|
+
if (/raw/i.test(outputFormat)) return "audio/L16";
|
|
134
|
+
return "application/octet-stream";
|
|
135
|
+
}
|
|
90
136
|
function resolveOutputFormat(outputFormat) {
|
|
91
137
|
const resolvedFormat = OUTPUT_FORMATS[outputFormat];
|
|
92
138
|
if (resolvedFormat === void 0) {
|
|
@@ -96,6 +142,7 @@ function resolveOutputFormat(outputFormat) {
|
|
|
96
142
|
}
|
|
97
143
|
|
|
98
144
|
// src/speechConfig.ts
|
|
145
|
+
import { SpeechConfig } from "microsoft-cognitiveservices-speech-sdk";
|
|
99
146
|
function resolveEndpoint(config) {
|
|
100
147
|
const endpoint = config.endpoint?.trim() || "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
|
|
101
148
|
return endpoint.replace(/\{region\}/g, encodeURIComponent(config.region));
|
|
@@ -148,6 +195,105 @@ function parseWav(buffer) {
|
|
|
148
195
|
}
|
|
149
196
|
return { chunks, data, format };
|
|
150
197
|
}
|
|
198
|
+
function formatNumber(format, pattern, fallback) {
|
|
199
|
+
const match = pattern.exec(format);
|
|
200
|
+
return match?.[1] ? Number(match[1]) : fallback;
|
|
201
|
+
}
|
|
202
|
+
function formatChannels(format, fallback) {
|
|
203
|
+
if (/stereo|2ch|dual/i.test(format)) return 2;
|
|
204
|
+
if (/mono|1ch/i.test(format)) return 1;
|
|
205
|
+
return fallback;
|
|
206
|
+
}
|
|
207
|
+
function formatAudioSpecification(format) {
|
|
208
|
+
const sampleRate = formatNumber(format, /(\d+)(?:khz|kHz|hz|Hz)/, 0);
|
|
209
|
+
const channels = formatChannels(format, 0);
|
|
210
|
+
const bitrateMatch = /(\d+)(?:kbitrate|kbps|kbits?)/i.exec(format);
|
|
211
|
+
const bitrate = bitrateMatch?.[1] ? Number(bitrateMatch[1]) * 1e3 : void 0;
|
|
212
|
+
const codec = /mp3|mpeg/i.test(format) ? "mp3" : /opus/i.test(format) ? "opus" : /silk/i.test(format) ? "silk" : /pcm|mulaw|alaw|siren/i.test(format) ? "pcm" : "unknown";
|
|
213
|
+
return {
|
|
214
|
+
format,
|
|
215
|
+
mimeType: resolveMimeType(format),
|
|
216
|
+
codec,
|
|
217
|
+
sampleRate,
|
|
218
|
+
channels,
|
|
219
|
+
...bitrate ? { bitrate } : {},
|
|
220
|
+
isCompressed: codec === "mp3" || codec === "opus" || codec === "silk"
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function parseMp3Specification(buffer, format) {
|
|
224
|
+
const bytes = stripMp3Tags(buffer);
|
|
225
|
+
const bitrates = [
|
|
226
|
+
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
227
|
+
[0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0],
|
|
228
|
+
[0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0]
|
|
229
|
+
];
|
|
230
|
+
const sampleRates = [
|
|
231
|
+
[44100, 48e3, 32e3],
|
|
232
|
+
[22050, 24e3, 16e3],
|
|
233
|
+
[11025, 12e3, 8e3]
|
|
234
|
+
];
|
|
235
|
+
for (let index = 0; index + 4 <= bytes.length; index += 1) {
|
|
236
|
+
if (bytes[index] !== 255 || (bytes[index + 1] ?? 0) < 224) continue;
|
|
237
|
+
const header = bytes[index + 1] ?? 0;
|
|
238
|
+
const versionBits = header >> 3 & 3;
|
|
239
|
+
const layer = header >> 1 & 3;
|
|
240
|
+
const bitrateIndex = (bytes[index + 2] ?? 0) >> 4;
|
|
241
|
+
const sampleIndex = (bytes[index + 2] ?? 0) >> 2 & 3;
|
|
242
|
+
if (versionBits === 1 || layer !== 1 || bitrateIndex === 0 || bitrateIndex === 15 || sampleIndex === 3) continue;
|
|
243
|
+
const versionIndex = versionBits === 3 ? 0 : versionBits === 2 ? 1 : 2;
|
|
244
|
+
const bitrateTable = versionBits === 3 ? bitrates[1] : bitrates[2];
|
|
245
|
+
const sampleRate = sampleRates[versionIndex]?.[sampleIndex] ?? 0;
|
|
246
|
+
const bitrateKbps = bitrateTable?.[bitrateIndex] ?? 0;
|
|
247
|
+
if (!sampleRate || !bitrateKbps) continue;
|
|
248
|
+
return {
|
|
249
|
+
format,
|
|
250
|
+
mimeType: "audio/mpeg",
|
|
251
|
+
codec: "mp3",
|
|
252
|
+
sampleRate,
|
|
253
|
+
channels: (bytes[index + 3] ?? 0) >> 6 === 3 ? 1 : 2,
|
|
254
|
+
bitrate: bitrateKbps * 1e3,
|
|
255
|
+
isCompressed: true
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return void 0;
|
|
259
|
+
}
|
|
260
|
+
function inspectAudioSpecification(buffer, format) {
|
|
261
|
+
if (isWavFormat(format) || ascii(new Uint8Array(buffer), 0, "RIFF")) {
|
|
262
|
+
const parsed = parseWav(buffer);
|
|
263
|
+
if (parsed.format.byteLength < 16) throw new Error("Invalid WAV fmt chunk.");
|
|
264
|
+
const view = new DataView(parsed.format.buffer, parsed.format.byteOffset, parsed.format.byteLength);
|
|
265
|
+
const sampleRate = view.getUint32(4, true);
|
|
266
|
+
const channels = view.getUint16(2, true);
|
|
267
|
+
const bitsPerSample = parsed.format.byteLength >= 16 ? view.getUint16(14, true) : 0;
|
|
268
|
+
const formatCode = view.getUint16(0, true);
|
|
269
|
+
return {
|
|
270
|
+
format,
|
|
271
|
+
mimeType: "audio/wav",
|
|
272
|
+
codec: formatCode === 1 ? "pcm" : "unknown",
|
|
273
|
+
sampleRate,
|
|
274
|
+
channels,
|
|
275
|
+
...sampleRate && channels && bitsPerSample ? { bitrate: sampleRate * channels * bitsPerSample } : {},
|
|
276
|
+
isCompressed: formatCode !== 1
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (isMp3Format(format)) return parseMp3Specification(buffer, format) ?? formatAudioSpecification(format);
|
|
280
|
+
return formatAudioSpecification(format);
|
|
281
|
+
}
|
|
282
|
+
function validateAudioSpecifications(specs) {
|
|
283
|
+
const first = specs[0];
|
|
284
|
+
if (!first) return;
|
|
285
|
+
const mismatch = specs.find(
|
|
286
|
+
(spec) => spec.sampleRate !== first.sampleRate || spec.channels !== first.channels || first.bitrate !== void 0 && spec.bitrate !== void 0 && spec.bitrate !== first.bitrate
|
|
287
|
+
);
|
|
288
|
+
if (mismatch)
|
|
289
|
+
throw new AudioFormatMismatchError(
|
|
290
|
+
`Audio chunks have incompatible specifications: ${first.sampleRate}Hz/${first.channels}ch versus ${mismatch.sampleRate}Hz/${mismatch.channels}ch.`,
|
|
291
|
+
specs
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
function isAudioFormatMismatch(error) {
|
|
295
|
+
return error instanceof AudioFormatMismatchError || error !== null && typeof error === "object" && "kind" in error && error.kind === "audio-format-mismatch";
|
|
296
|
+
}
|
|
151
297
|
function writeUint32(target, offset, value) {
|
|
152
298
|
new DataView(target.buffer).setUint32(offset, value, true);
|
|
153
299
|
}
|
|
@@ -229,28 +375,37 @@ function resolveMergeAudioFormat(format) {
|
|
|
229
375
|
function canMergeAudioFormat(format) {
|
|
230
376
|
return resolveMergeAudioFormat(format) !== void 0;
|
|
231
377
|
}
|
|
232
|
-
function mergeAudioBuffers(buffers,
|
|
233
|
-
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
378
|
+
function mergeAudioBuffers(buffers, options) {
|
|
379
|
+
const format = typeof options === "string" ? options : options?.format;
|
|
380
|
+
if (!format) throw new UnsupportedMergeFormatError("");
|
|
381
|
+
try {
|
|
382
|
+
validateAudioSpecifications(buffers.map((buffer) => inspectAudioSpecification(buffer, format)));
|
|
383
|
+
if (isWavFormat(format)) return mergeWavBuffers(buffers);
|
|
384
|
+
if (isMp3Format(format)) {
|
|
385
|
+
const parts = buffers.map(stripMp3Tags);
|
|
386
|
+
const output = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0));
|
|
387
|
+
let offset = 0;
|
|
388
|
+
for (const part of parts) {
|
|
389
|
+
output.set(part, offset);
|
|
390
|
+
offset += part.byteLength;
|
|
391
|
+
}
|
|
392
|
+
return output.buffer;
|
|
241
393
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
394
|
+
if (isRawFormat(format)) {
|
|
395
|
+
const output = new Uint8Array(buffers.reduce((total, buffer) => total + buffer.byteLength, 0));
|
|
396
|
+
let offset = 0;
|
|
397
|
+
for (const buffer of buffers) {
|
|
398
|
+
output.set(new Uint8Array(buffer), offset);
|
|
399
|
+
offset += buffer.byteLength;
|
|
400
|
+
}
|
|
401
|
+
return output.buffer;
|
|
250
402
|
}
|
|
251
|
-
|
|
403
|
+
throw new UnsupportedMergeFormatError(format);
|
|
404
|
+
} catch (error) {
|
|
405
|
+
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
406
|
+
throw error;
|
|
407
|
+
throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
|
|
252
408
|
}
|
|
253
|
-
throw new UnsupportedMergeFormatError(format);
|
|
254
409
|
}
|
|
255
410
|
function closeSpeechResources(speechConfig, synthesizer) {
|
|
256
411
|
try {
|
|
@@ -265,7 +420,7 @@ function closeSpeechResources(speechConfig, synthesizer) {
|
|
|
265
420
|
var ticksToMilliseconds = (ticks) => Math.max(0, ticks) / 1e4;
|
|
266
421
|
async function synthesizeSsml(ssml, config) {
|
|
267
422
|
if (config.signal?.aborted) {
|
|
268
|
-
throw
|
|
423
|
+
throw new SynthesisCancelledError();
|
|
269
424
|
}
|
|
270
425
|
const speechConfig = createSpeechConfig(config);
|
|
271
426
|
const synthesizer = new SpeechSDK2.SpeechSynthesizer(speechConfig, null);
|
|
@@ -288,23 +443,104 @@ async function synthesizeSsml(ssml, config) {
|
|
|
288
443
|
settled = true;
|
|
289
444
|
cleanup();
|
|
290
445
|
closeResources();
|
|
291
|
-
reject(
|
|
446
|
+
reject(toSynthesisError(error));
|
|
292
447
|
};
|
|
293
448
|
const boundaries = [];
|
|
294
449
|
const visemes = [];
|
|
295
450
|
const bookmarks = [];
|
|
451
|
+
let sourceEventCursor = 0;
|
|
452
|
+
let generatedSourceMap;
|
|
453
|
+
if (!config.sourceTextSegments && !config.sourceMarkers) {
|
|
454
|
+
try {
|
|
455
|
+
generatedSourceMap = getSsmlSourceMap(ssml);
|
|
456
|
+
} catch {
|
|
457
|
+
generatedSourceMap = void 0;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const sourceBaseOffset = config.sourceTextRange?.start ?? 0;
|
|
461
|
+
const sourceSegments = config.sourceTextSegments ?? generatedSourceMap?.segments.map((segment) => ({
|
|
462
|
+
...segment,
|
|
463
|
+
range: {
|
|
464
|
+
start: segment.range.start + sourceBaseOffset,
|
|
465
|
+
end: segment.range.end + sourceBaseOffset
|
|
466
|
+
},
|
|
467
|
+
sourceNodePath: [...segment.sourceNodePath]
|
|
468
|
+
})) ?? [];
|
|
469
|
+
const sourceMarkers = config.sourceMarkers ?? generatedSourceMap?.markers.map((marker) => ({
|
|
470
|
+
...marker,
|
|
471
|
+
originalTextRange: {
|
|
472
|
+
start: marker.originalTextRange.start + sourceBaseOffset,
|
|
473
|
+
end: marker.originalTextRange.end + sourceBaseOffset
|
|
474
|
+
},
|
|
475
|
+
sourceNodePath: [...marker.sourceNodePath]
|
|
476
|
+
})) ?? [];
|
|
477
|
+
const sourceText = sourceSegments.map((segment) => segment.text).join("");
|
|
478
|
+
const mapSourceEvent = (text, offsetHint, markerName) => {
|
|
479
|
+
const marker = markerName ? sourceMarkers.find((candidate) => candidate.name === markerName) : void 0;
|
|
480
|
+
if (marker) {
|
|
481
|
+
return {
|
|
482
|
+
originalTextRange: { ...marker.originalTextRange },
|
|
483
|
+
sourceNodePath: [...marker.sourceNodePath],
|
|
484
|
+
textRange: { ...marker.originalTextRange },
|
|
485
|
+
mappingStatus: "exact"
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
if (sourceSegments.length === 0 && !config.sourceTextRange && !config.sourceNodePath) {
|
|
489
|
+
const unmapped = { mappingStatus: "unmapped" };
|
|
490
|
+
Object.defineProperty(unmapped, "mappingStatus", { value: "unmapped", enumerable: false });
|
|
491
|
+
return unmapped;
|
|
492
|
+
}
|
|
493
|
+
const value = text ?? "";
|
|
494
|
+
let localStart = Number.isFinite(offsetHint) && (offsetHint ?? 0) >= 0 ? offsetHint : -1;
|
|
495
|
+
let mappingStatus = "exact";
|
|
496
|
+
if (value && localStart >= 0 && sourceText.slice(localStart, localStart + value.length) !== value) {
|
|
497
|
+
localStart = -1;
|
|
498
|
+
mappingStatus = "fallback";
|
|
499
|
+
}
|
|
500
|
+
if (localStart < 0 || localStart > sourceText.length) {
|
|
501
|
+
localStart = value ? sourceText.indexOf(value, sourceEventCursor) : sourceEventCursor;
|
|
502
|
+
if (localStart < 0) localStart = value ? sourceText.indexOf(value) : sourceEventCursor;
|
|
503
|
+
mappingStatus = "fallback";
|
|
504
|
+
}
|
|
505
|
+
localStart = Math.max(0, localStart);
|
|
506
|
+
const localEnd = Math.min(sourceText.length, localStart + value.length);
|
|
507
|
+
sourceEventCursor = Math.max(sourceEventCursor, localEnd);
|
|
508
|
+
const baseStart = config.sourceTextRange?.start ?? sourceSegments[0]?.range.start ?? 0;
|
|
509
|
+
const fallbackRange = { start: baseStart + localStart, end: baseStart + localEnd };
|
|
510
|
+
const segment = sourceSegments.find(({ range }) => range.start <= fallbackRange.start && range.end > fallbackRange.start) ?? sourceSegments.find(({ range }) => range.end > fallbackRange.start) ?? (value.length === 0 ? sourceSegments.find(({ range }) => range.start <= fallbackRange.start && range.end >= fallbackRange.start) : void 0);
|
|
511
|
+
return {
|
|
512
|
+
originalTextRange: { ...fallbackRange },
|
|
513
|
+
textRange: { ...fallbackRange },
|
|
514
|
+
...segment ? { sourceNodePath: [...segment.sourceNodePath] } : config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
|
|
515
|
+
mappingStatus: segment || config.sourceTextRange || config.sourceNodePath ? mappingStatus : "unmapped"
|
|
516
|
+
};
|
|
517
|
+
};
|
|
296
518
|
synthesizer.wordBoundary = (_sender, event) => {
|
|
297
519
|
boundaries.push({
|
|
298
520
|
text: event.text,
|
|
299
521
|
audioOffsetMs: ticksToMilliseconds(event.audioOffset),
|
|
300
|
-
durationMs: ticksToMilliseconds(event.duration)
|
|
522
|
+
durationMs: ticksToMilliseconds(event.duration),
|
|
523
|
+
...mapSourceEvent(
|
|
524
|
+
event.text,
|
|
525
|
+
event.textOffset
|
|
526
|
+
)
|
|
301
527
|
});
|
|
302
528
|
};
|
|
303
529
|
synthesizer.visemeReceived = (_sender, event) => {
|
|
304
|
-
|
|
530
|
+
const eventWithOffset = event;
|
|
531
|
+
visemes.push({
|
|
532
|
+
visemeId: event.visemeId,
|
|
533
|
+
audioOffsetMs: ticksToMilliseconds(event.audioOffset),
|
|
534
|
+
...mapSourceEvent(void 0, eventWithOffset.textOffset)
|
|
535
|
+
});
|
|
305
536
|
};
|
|
306
537
|
synthesizer.bookmarkReached = (_sender, event) => {
|
|
307
|
-
|
|
538
|
+
const eventWithOffset = event;
|
|
539
|
+
bookmarks.push({
|
|
540
|
+
name: event.text,
|
|
541
|
+
audioOffsetMs: ticksToMilliseconds(event.audioOffset),
|
|
542
|
+
...mapSourceEvent(void 0, eventWithOffset.textOffset, event.text)
|
|
543
|
+
});
|
|
308
544
|
};
|
|
309
545
|
const cb = (result) => {
|
|
310
546
|
if (settled) return;
|
|
@@ -325,20 +561,27 @@ async function synthesizeSsml(ssml, config) {
|
|
|
325
561
|
);
|
|
326
562
|
const durationMs = result.audioDuration ? ticksToMilliseconds(result.audioDuration) : eventDurationMs;
|
|
327
563
|
const requestId = result.resultId;
|
|
328
|
-
const addSourceMetadata = (event) =>
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
564
|
+
const addSourceMetadata = (event) => {
|
|
565
|
+
const mapped = {
|
|
566
|
+
...event,
|
|
567
|
+
...config.sourceTextRange && !("textRange" in event) ? { textRange: { ...config.sourceTextRange } } : {},
|
|
568
|
+
...config.sourceTextRange && !("originalTextRange" in event) ? { originalTextRange: { ...config.sourceTextRange } } : {},
|
|
569
|
+
...config.chunkIndex !== void 0 ? { chunkIndex: config.chunkIndex } : {},
|
|
570
|
+
...config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
|
|
571
|
+
...requestId ? { requestId } : {}
|
|
572
|
+
};
|
|
573
|
+
if (event.mappingStatus === "unmapped")
|
|
574
|
+
Object.defineProperty(mapped, "mappingStatus", { value: "unmapped", enumerable: false });
|
|
575
|
+
return mapped;
|
|
576
|
+
};
|
|
336
577
|
const sourceBoundaries = boundaries.map((boundary) => addSourceMetadata(boundary));
|
|
337
578
|
const sourceVisemes = visemes.map((viseme) => addSourceMetadata(viseme));
|
|
338
579
|
const sourceBookmarks = bookmarks.map((bookmark) => addSourceMetadata(bookmark));
|
|
339
580
|
resolve({
|
|
340
581
|
audioData: result.audioData,
|
|
341
582
|
durationMs,
|
|
583
|
+
audioSpec: inspectAudioSpecification(result.audioData, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
|
|
584
|
+
mimeType: resolveMimeType(config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
|
|
342
585
|
...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
|
|
343
586
|
...requestId ? { requestId } : {},
|
|
344
587
|
...sourceBoundaries.length > 0 ? { boundaries: sourceBoundaries, wordBoundary: sourceBoundaries, wordBoundaries: sourceBoundaries } : {},
|
|
@@ -348,12 +591,12 @@ async function synthesizeSsml(ssml, config) {
|
|
|
348
591
|
};
|
|
349
592
|
try {
|
|
350
593
|
if (config.signal) {
|
|
351
|
-
abortHandler = () => rejectWithError(
|
|
594
|
+
abortHandler = () => rejectWithError(new SynthesisCancelledError());
|
|
352
595
|
config.signal.addEventListener("abort", abortHandler, { once: true });
|
|
353
596
|
}
|
|
354
597
|
if (config.timeoutMs !== void 0 && config.timeoutMs > 0) {
|
|
355
598
|
timeout = setTimeout(
|
|
356
|
-
() => rejectWithError(`Speech synthesis timed out after ${config.timeoutMs} ms.`),
|
|
599
|
+
() => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeoutMs} ms.`)),
|
|
357
600
|
config.timeoutMs
|
|
358
601
|
);
|
|
359
602
|
}
|
|
@@ -363,8 +606,66 @@ async function synthesizeSsml(ssml, config) {
|
|
|
363
606
|
}
|
|
364
607
|
});
|
|
365
608
|
}
|
|
609
|
+
function isRetryableSynthesisError(error) {
|
|
610
|
+
if (error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError) return false;
|
|
611
|
+
if (error instanceof AzureTtsError && error.status !== 0)
|
|
612
|
+
return error.status === 429 || error.status >= 500 && error.status < 600;
|
|
613
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
614
|
+
if (/\b4\d{2}\b/.test(message)) return false;
|
|
615
|
+
const status = error && typeof error === "object" && "status" in error ? error.status : void 0;
|
|
616
|
+
if (typeof status === "number") return status === 429 || status >= 500 && status < 600;
|
|
617
|
+
return /network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message);
|
|
618
|
+
}
|
|
619
|
+
function retryDelay(options, retryAttempt) {
|
|
620
|
+
const base = Math.min(options.maxDelayMs, options.initialDelayMs * 2 ** Math.max(0, retryAttempt - 1));
|
|
621
|
+
return Math.floor(Math.random() * (base + 1));
|
|
622
|
+
}
|
|
623
|
+
function resolveConcurrency(value, total) {
|
|
624
|
+
if (value === void 0) return 1;
|
|
625
|
+
if (value === Infinity) return Math.max(1, total);
|
|
626
|
+
return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
|
|
627
|
+
}
|
|
628
|
+
async function waitForRetry(delayMs, signal) {
|
|
629
|
+
if (signal?.aborted) throw new SynthesisCancelledError();
|
|
630
|
+
if (delayMs <= 0) return;
|
|
631
|
+
await new Promise((resolve, reject) => {
|
|
632
|
+
let timer;
|
|
633
|
+
const abort = () => {
|
|
634
|
+
clearTimeout(timer);
|
|
635
|
+
signal?.removeEventListener("abort", abort);
|
|
636
|
+
reject(new SynthesisCancelledError());
|
|
637
|
+
};
|
|
638
|
+
timer = setTimeout(() => {
|
|
639
|
+
signal?.removeEventListener("abort", abort);
|
|
640
|
+
resolve();
|
|
641
|
+
}, delayMs);
|
|
642
|
+
if (signal) {
|
|
643
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
async function synthesizeWithRetry(ssml, config, retryOptions, onRetry) {
|
|
648
|
+
const options = retryOptions ? {
|
|
649
|
+
maxRetries: Math.max(0, Math.floor(retryOptions.maxRetries)),
|
|
650
|
+
initialDelayMs: Math.max(0, retryOptions.initialDelayMs),
|
|
651
|
+
maxDelayMs: Math.max(0, retryOptions.maxDelayMs)
|
|
652
|
+
} : void 0;
|
|
653
|
+
let attempt = 0;
|
|
654
|
+
while (true) {
|
|
655
|
+
if (config.signal?.aborted) throw new SynthesisCancelledError();
|
|
656
|
+
try {
|
|
657
|
+
return await synthesizeSsml(ssml, config);
|
|
658
|
+
} catch (error) {
|
|
659
|
+
if (!options || attempt >= options.maxRetries || !isRetryableSynthesisError(error)) throw error;
|
|
660
|
+
attempt += 1;
|
|
661
|
+
const delayMs = retryDelay(options, attempt);
|
|
662
|
+
onRetry(attempt, delayMs);
|
|
663
|
+
await waitForRetry(delayMs, config.signal);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
366
667
|
async function synthesizeSsmlChunks(chunks, config) {
|
|
367
|
-
const results =
|
|
668
|
+
const results = new Array(chunks.length);
|
|
368
669
|
const totalChunks = chunks.length;
|
|
369
670
|
const report = (event) => config.onProgress?.(event);
|
|
370
671
|
for (const [index, chunk] of chunks.entries()) {
|
|
@@ -379,71 +680,90 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
379
680
|
durationMs: 0
|
|
380
681
|
});
|
|
381
682
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
durationMs: 0
|
|
392
|
-
});
|
|
393
|
-
const startedAt = Date.now();
|
|
394
|
-
try {
|
|
395
|
-
const result = await synthesizeSsml(input.ssml, {
|
|
396
|
-
...config,
|
|
397
|
-
...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
|
|
398
|
-
...input.sourceNodePath ? { sourceNodePath: input.sourceNodePath } : {},
|
|
399
|
-
chunkIndex: index,
|
|
400
|
-
onProgress: void 0
|
|
401
|
-
});
|
|
402
|
-
results.push(result);
|
|
403
|
-
report({
|
|
404
|
-
currentChunk: index + 1,
|
|
405
|
-
totalChunks,
|
|
406
|
-
percent: totalChunks === 0 ? 100 : Math.round((index + 1) / totalChunks * 100),
|
|
407
|
-
chunkIndex: index,
|
|
408
|
-
originalTextRange: input.originalTextRange,
|
|
409
|
-
status: "success",
|
|
410
|
-
durationMs: Date.now() - startedAt
|
|
411
|
-
});
|
|
412
|
-
} catch (error) {
|
|
683
|
+
let completed = 0;
|
|
684
|
+
let nextIndex = 0;
|
|
685
|
+
const concurrency = resolveConcurrency(config.concurrency, chunks.length);
|
|
686
|
+
const worker = async () => {
|
|
687
|
+
while (true) {
|
|
688
|
+
const index = nextIndex++;
|
|
689
|
+
if (index >= chunks.length) return;
|
|
690
|
+
const chunk = chunks[index];
|
|
691
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
413
692
|
report({
|
|
414
|
-
currentChunk:
|
|
693
|
+
currentChunk: completed,
|
|
415
694
|
totalChunks,
|
|
416
|
-
percent: totalChunks === 0 ? 100 : Math.round(
|
|
695
|
+
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
417
696
|
chunkIndex: index,
|
|
418
697
|
originalTextRange: input.originalTextRange,
|
|
419
|
-
status: "
|
|
420
|
-
durationMs:
|
|
421
|
-
error
|
|
698
|
+
status: "synthesizing",
|
|
699
|
+
durationMs: 0
|
|
422
700
|
});
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
701
|
+
const startedAt = Date.now();
|
|
702
|
+
try {
|
|
703
|
+
const result = await synthesizeWithRetry(
|
|
704
|
+
input.ssml,
|
|
705
|
+
{
|
|
706
|
+
...config,
|
|
707
|
+
...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
|
|
708
|
+
...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
|
|
709
|
+
...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
|
|
710
|
+
...input.sourceMarkers ? { sourceMarkers: input.sourceMarkers } : {},
|
|
711
|
+
chunkIndex: index,
|
|
712
|
+
onProgress: void 0
|
|
713
|
+
},
|
|
714
|
+
config.retryOptions,
|
|
715
|
+
(retryAttempt, nextRetryDelayMs) => report({
|
|
716
|
+
currentChunk: completed,
|
|
717
|
+
totalChunks,
|
|
718
|
+
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
719
|
+
chunkIndex: index,
|
|
720
|
+
originalTextRange: input.originalTextRange,
|
|
721
|
+
status: "synthesizing",
|
|
722
|
+
durationMs: Date.now() - startedAt,
|
|
723
|
+
retryAttempt,
|
|
724
|
+
nextRetryDelayMs,
|
|
725
|
+
isRetrying: true
|
|
726
|
+
})
|
|
727
|
+
);
|
|
728
|
+
results[index] = result;
|
|
729
|
+
completed += 1;
|
|
730
|
+
report({
|
|
731
|
+
currentChunk: completed,
|
|
732
|
+
totalChunks,
|
|
733
|
+
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
734
|
+
chunkIndex: index,
|
|
735
|
+
originalTextRange: input.originalTextRange,
|
|
736
|
+
status: "success",
|
|
737
|
+
durationMs: Date.now() - startedAt
|
|
738
|
+
});
|
|
739
|
+
} catch (error) {
|
|
740
|
+
report({
|
|
741
|
+
currentChunk: completed,
|
|
742
|
+
totalChunks,
|
|
743
|
+
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
744
|
+
chunkIndex: index,
|
|
745
|
+
originalTextRange: input.originalTextRange,
|
|
746
|
+
status: "failed",
|
|
747
|
+
durationMs: Date.now() - startedAt,
|
|
748
|
+
error
|
|
749
|
+
});
|
|
750
|
+
throw error;
|
|
751
|
+
}
|
|
440
752
|
}
|
|
441
|
-
}
|
|
753
|
+
};
|
|
754
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
755
|
+
const orderedResults = results.filter((result) => result !== void 0);
|
|
756
|
+
return mergeSynthesisResults(orderedResults, {
|
|
757
|
+
format: config.outputFormat ?? DEFAULT_OUTPUT_FORMAT,
|
|
758
|
+
signal: config.signal
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
function createMergedResult(results, audioData, format, audioSpec, outputMimeType) {
|
|
442
762
|
const boundaries = [];
|
|
443
763
|
const visemes = [];
|
|
444
764
|
const bookmarks = [];
|
|
445
765
|
let durationOffset = 0;
|
|
446
|
-
for (const result of results) {
|
|
766
|
+
for (const [resultIndex, result] of results.entries()) {
|
|
447
767
|
const chunkBoundaries = result.boundaries && result.boundaries.length > 0 ? result.boundaries : result.wordBoundary ?? result.wordBoundaries ?? [];
|
|
448
768
|
for (const boundary of chunkBoundaries) {
|
|
449
769
|
const textRange = boundary.textRange ?? result.textRange;
|
|
@@ -453,11 +773,12 @@ function mergeSynthesisResults(results, format) {
|
|
|
453
773
|
...boundary,
|
|
454
774
|
audioOffsetMs: boundary.audioOffsetMs + durationOffset,
|
|
455
775
|
chunkAudioOffsetMs: boundary.chunkAudioOffsetMs ?? boundary.audioOffsetMs,
|
|
456
|
-
...boundary.chunkIndex === void 0 ? { chunkIndex:
|
|
776
|
+
...boundary.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
|
|
457
777
|
...boundary.sourceNodePath ? { sourceNodePath: [...boundary.sourceNodePath] } : {},
|
|
458
778
|
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
459
779
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
460
|
-
...requestId ? { requestId } : {}
|
|
780
|
+
...requestId ? { requestId } : {},
|
|
781
|
+
mappingStatus: boundary.mappingStatus ?? "unmapped"
|
|
461
782
|
});
|
|
462
783
|
}
|
|
463
784
|
for (const viseme of result.visemes ?? []) {
|
|
@@ -468,11 +789,12 @@ function mergeSynthesisResults(results, format) {
|
|
|
468
789
|
...viseme,
|
|
469
790
|
audioOffsetMs: viseme.audioOffsetMs + durationOffset,
|
|
470
791
|
chunkAudioOffsetMs: viseme.chunkAudioOffsetMs ?? viseme.audioOffsetMs,
|
|
471
|
-
...viseme.chunkIndex === void 0 ? { chunkIndex:
|
|
792
|
+
...viseme.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
|
|
472
793
|
...viseme.sourceNodePath ? { sourceNodePath: [...viseme.sourceNodePath] } : {},
|
|
473
794
|
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
474
795
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
475
|
-
...requestId ? { requestId } : {}
|
|
796
|
+
...requestId ? { requestId } : {},
|
|
797
|
+
mappingStatus: viseme.mappingStatus ?? "unmapped"
|
|
476
798
|
});
|
|
477
799
|
}
|
|
478
800
|
for (const bookmark of result.bookmarks ?? []) {
|
|
@@ -483,18 +805,22 @@ function mergeSynthesisResults(results, format) {
|
|
|
483
805
|
...bookmark,
|
|
484
806
|
audioOffsetMs: bookmark.audioOffsetMs + durationOffset,
|
|
485
807
|
chunkAudioOffsetMs: bookmark.chunkAudioOffsetMs ?? bookmark.audioOffsetMs,
|
|
486
|
-
...bookmark.chunkIndex === void 0 ? { chunkIndex:
|
|
808
|
+
...bookmark.chunkIndex === void 0 ? { chunkIndex: resultIndex } : {},
|
|
487
809
|
...bookmark.sourceNodePath ? { sourceNodePath: [...bookmark.sourceNodePath] } : {},
|
|
488
810
|
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
489
811
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
490
|
-
...requestId ? { requestId } : {}
|
|
812
|
+
...requestId ? { requestId } : {},
|
|
813
|
+
mappingStatus: bookmark.mappingStatus ?? "unmapped"
|
|
491
814
|
});
|
|
492
815
|
}
|
|
493
816
|
durationOffset += Math.max(0, result.durationMs);
|
|
494
817
|
}
|
|
495
818
|
return {
|
|
496
|
-
audioData
|
|
819
|
+
audioData,
|
|
497
820
|
durationMs: durationOffset,
|
|
821
|
+
mimeType: resolveMimeType(format),
|
|
822
|
+
audioSpec: audioSpec ?? formatAudioSpecification(format),
|
|
823
|
+
...outputMimeType ? { mimeType: outputMimeType } : {},
|
|
498
824
|
...boundaries.length > 0 ? { boundaries, wordBoundary: boundaries, wordBoundaries: boundaries } : {},
|
|
499
825
|
...visemes.length > 0 ? { visemes } : {},
|
|
500
826
|
...bookmarks.length > 0 ? { bookmarks } : {},
|
|
@@ -502,46 +828,174 @@ function mergeSynthesisResults(results, format) {
|
|
|
502
828
|
...results.length === 1 && results[0]?.textRange ? { textRange: { ...results[0].textRange } } : {}
|
|
503
829
|
};
|
|
504
830
|
}
|
|
831
|
+
function mergeSynthesisResults(results, options) {
|
|
832
|
+
const resolvedOptions = typeof options === "string" ? { format: options } : options;
|
|
833
|
+
const format = resolvedOptions?.format;
|
|
834
|
+
if (!format) throw new UnsupportedMergeFormatError("");
|
|
835
|
+
const buffers = results.map((result) => result.audioData);
|
|
836
|
+
const inputSpecs = results.map((result) => result.audioSpec ?? inspectAudioSpecification(result.audioData, format));
|
|
837
|
+
validateAudioSpecifications(inputSpecs);
|
|
838
|
+
const signal = resolvedOptions.signal ?? new AbortController().signal;
|
|
839
|
+
if (signal.aborted) throw new SynthesisCancelledError();
|
|
840
|
+
if (resolvedOptions.customMerger) {
|
|
841
|
+
return Promise.resolve().then(
|
|
842
|
+
() => resolvedOptions.customMerger?.(buffers, {
|
|
843
|
+
format,
|
|
844
|
+
outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
845
|
+
inputSpecs,
|
|
846
|
+
signal
|
|
847
|
+
})
|
|
848
|
+
).then((merged) => {
|
|
849
|
+
if (!merged) throw new MergeError("The custom audio merger returned no audio buffer.");
|
|
850
|
+
if (!(merged instanceof ArrayBuffer) || buffers.some((buffer) => buffer.byteLength > 0) && merged.byteLength === 0)
|
|
851
|
+
throw new MergeError("The custom audio merger returned an invalid audio buffer.");
|
|
852
|
+
if (signal.aborted) throw new SynthesisCancelledError();
|
|
853
|
+
return createMergedResult(
|
|
854
|
+
results,
|
|
855
|
+
merged,
|
|
856
|
+
format,
|
|
857
|
+
inspectAudioSpecification(merged, format),
|
|
858
|
+
resolvedOptions.outputMimeType
|
|
859
|
+
);
|
|
860
|
+
}).catch((error) => {
|
|
861
|
+
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
862
|
+
throw error;
|
|
863
|
+
throw new MergeError(`Custom audio merger failed for format "${format}".`, error);
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
try {
|
|
867
|
+
return createMergedResult(
|
|
868
|
+
results,
|
|
869
|
+
mergeAudioBuffers(buffers, { format }),
|
|
870
|
+
format,
|
|
871
|
+
inputSpecs[0],
|
|
872
|
+
resolvedOptions.outputMimeType
|
|
873
|
+
);
|
|
874
|
+
} catch (error) {
|
|
875
|
+
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
876
|
+
throw error;
|
|
877
|
+
throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
505
880
|
async function synthesizeSpeech(ssml, config) {
|
|
506
881
|
return (await synthesizeSsml(ssml, config)).audioData;
|
|
507
882
|
}
|
|
508
883
|
|
|
509
884
|
// src/safe.ts
|
|
510
|
-
import {
|
|
885
|
+
import {
|
|
886
|
+
createAzureUrlValidatorRunner,
|
|
887
|
+
validateAzureSsml
|
|
888
|
+
} from "@ssml-builder-js/ssml-core";
|
|
511
889
|
var ChunkValidationError = class extends Error {
|
|
512
890
|
constructor(chunkIndex, diagnostics) {
|
|
513
891
|
super(`SSML validation failed for chunk ${chunkIndex}; the Azure Speech API was not called.`);
|
|
514
|
-
this.kind = "
|
|
892
|
+
this.kind = "validation-error";
|
|
515
893
|
this.name = "ChunkValidationError";
|
|
516
894
|
this.chunkIndex = chunkIndex;
|
|
517
895
|
this.diagnostics = diagnostics;
|
|
518
896
|
}
|
|
519
897
|
};
|
|
898
|
+
function failure(error) {
|
|
899
|
+
return { ok: false, success: false, status: error.kind, error };
|
|
900
|
+
}
|
|
901
|
+
function isRetryable(error) {
|
|
902
|
+
if (error instanceof Error && /cancel|abort|tim(?:e|ed) ?out/i.test(error.message)) return false;
|
|
903
|
+
const status = error && typeof error === "object" && "status" in error ? error.status : void 0;
|
|
904
|
+
if (typeof status === "number" && status !== 0) return status === 429 || status >= 500 && status < 600;
|
|
905
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
906
|
+
if (/\b4\d{2}\b/.test(message)) return false;
|
|
907
|
+
return /network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message);
|
|
908
|
+
}
|
|
909
|
+
function delayForRetry(options, attempt) {
|
|
910
|
+
const maxDelay = Math.max(0, options.maxDelayMs);
|
|
911
|
+
const base = Math.min(maxDelay, Math.max(0, options.initialDelayMs) * 2 ** Math.max(0, attempt - 1));
|
|
912
|
+
return Math.floor(Math.random() * (base + 1));
|
|
913
|
+
}
|
|
914
|
+
function resolveConcurrency2(value, total) {
|
|
915
|
+
if (value === void 0) return 1;
|
|
916
|
+
if (value === Infinity) return Math.max(1, total);
|
|
917
|
+
return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
|
|
918
|
+
}
|
|
919
|
+
async function retryableSynthesis(synthesize, options, signal, onRetry) {
|
|
920
|
+
const retry = options ? {
|
|
921
|
+
maxRetries: Math.max(0, Math.floor(options.maxRetries)),
|
|
922
|
+
initialDelayMs: options.initialDelayMs,
|
|
923
|
+
maxDelayMs: options.maxDelayMs
|
|
924
|
+
} : void 0;
|
|
925
|
+
let attempt = 0;
|
|
926
|
+
while (true) {
|
|
927
|
+
if (signal?.aborted) throw new Error("Speech synthesis was cancelled.");
|
|
928
|
+
try {
|
|
929
|
+
return await synthesize();
|
|
930
|
+
} catch (error) {
|
|
931
|
+
if (!retry || attempt >= retry.maxRetries || !isRetryable(error)) throw error;
|
|
932
|
+
attempt += 1;
|
|
933
|
+
const delayMs = delayForRetry(retry, attempt);
|
|
934
|
+
onRetry(attempt, delayMs);
|
|
935
|
+
if (delayMs > 0)
|
|
936
|
+
await new Promise((resolve, reject) => {
|
|
937
|
+
const timer = setTimeout(() => {
|
|
938
|
+
signal?.removeEventListener("abort", abort);
|
|
939
|
+
resolve();
|
|
940
|
+
}, delayMs);
|
|
941
|
+
const abort = () => {
|
|
942
|
+
clearTimeout(timer);
|
|
943
|
+
signal?.removeEventListener("abort", abort);
|
|
944
|
+
reject(new Error("Speech synthesis was cancelled."));
|
|
945
|
+
};
|
|
946
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
function sharedValidationOptions(options, signal) {
|
|
952
|
+
const validator = options.urlValidator ?? options.customUrlValidator;
|
|
953
|
+
if (!validator) return signal ? withValidationSignal(options, signal) : options;
|
|
954
|
+
const runner = createAzureUrlValidatorRunner(validator, {
|
|
955
|
+
...options.urlValidation ?? {},
|
|
956
|
+
...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
|
|
957
|
+
...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
|
|
958
|
+
...signal ? { signal } : {},
|
|
959
|
+
...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
|
|
960
|
+
});
|
|
961
|
+
return {
|
|
962
|
+
...withValidationSignal(options, signal),
|
|
963
|
+
urlValidatorRunner: runner
|
|
964
|
+
};
|
|
965
|
+
}
|
|
520
966
|
async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
521
|
-
const validationOptions = options.validation ?? options;
|
|
967
|
+
const validationOptions = sharedValidationOptions(options.validation ?? options, options.signal);
|
|
522
968
|
const diagnostics = await Promise.resolve(validateAzureSsml(ssml, validationOptions));
|
|
969
|
+
if (options.signal?.aborted) {
|
|
970
|
+
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
971
|
+
return failure(error);
|
|
972
|
+
}
|
|
523
973
|
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
524
974
|
if (errors.length > 0) {
|
|
525
|
-
return {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
kind: "validation",
|
|
531
|
-
message: "SSML validation failed; the Azure Speech API was not called.",
|
|
532
|
-
diagnostics: errors
|
|
533
|
-
}
|
|
534
|
-
};
|
|
975
|
+
return failure({
|
|
976
|
+
kind: "validation-error",
|
|
977
|
+
message: "SSML validation failed; the Azure Speech API was not called.",
|
|
978
|
+
diagnostics: errors
|
|
979
|
+
});
|
|
535
980
|
}
|
|
536
981
|
try {
|
|
537
|
-
return {
|
|
982
|
+
return {
|
|
983
|
+
ok: true,
|
|
984
|
+
success: true,
|
|
985
|
+
status: "success",
|
|
986
|
+
value: await client.synthesizeSsml(ssml, { signal: options.signal })
|
|
987
|
+
};
|
|
538
988
|
} catch (error) {
|
|
539
|
-
const
|
|
540
|
-
return
|
|
989
|
+
const synthesisError = toSynthesisError(error);
|
|
990
|
+
return failure(synthesisError);
|
|
541
991
|
}
|
|
542
992
|
}
|
|
543
993
|
async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
544
|
-
const validationOptions = options.validation ?? options;
|
|
994
|
+
const validationOptions = sharedValidationOptions(options.validation ?? options, options.signal);
|
|
995
|
+
if (options.signal?.aborted) {
|
|
996
|
+
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
997
|
+
return failure(error);
|
|
998
|
+
}
|
|
545
999
|
const pending = (index, status, error) => {
|
|
546
1000
|
options.onProgress?.({
|
|
547
1001
|
currentChunk: status === "success" ? index + 1 : index,
|
|
@@ -558,77 +1012,175 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
558
1012
|
pending(index, "pending");
|
|
559
1013
|
});
|
|
560
1014
|
const validations = await Promise.all(
|
|
561
|
-
chunks.map(async (chunk) => {
|
|
1015
|
+
chunks.map(async (chunk, index) => {
|
|
562
1016
|
const ssml = typeof chunk === "string" ? chunk : chunk.ssml;
|
|
563
|
-
const
|
|
1017
|
+
const sourceNodePath = typeof chunk === "string" ? options.sourceNodePath : chunk.sourceNodePath ?? options.sourceNodePath;
|
|
1018
|
+
const diagnostics = await Promise.resolve(
|
|
1019
|
+
validateAzureSsml(ssml, {
|
|
1020
|
+
...validationOptions,
|
|
1021
|
+
...sourceNodePath ? { sourceNodePath } : {},
|
|
1022
|
+
chunkIndex: index
|
|
1023
|
+
})
|
|
1024
|
+
);
|
|
564
1025
|
return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
565
1026
|
})
|
|
566
1027
|
);
|
|
567
1028
|
const firstInvalidIndex = validations.findIndex((diagnostics) => diagnostics.length > 0);
|
|
1029
|
+
if (options.signal?.aborted) {
|
|
1030
|
+
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
1031
|
+
return failure(error);
|
|
1032
|
+
}
|
|
568
1033
|
if (firstInvalidIndex >= 0) {
|
|
569
1034
|
const error = new ChunkValidationError(firstInvalidIndex, validations[firstInvalidIndex] ?? []);
|
|
570
1035
|
pending(firstInvalidIndex, "failed", error);
|
|
571
|
-
return
|
|
1036
|
+
return failure(error);
|
|
572
1037
|
}
|
|
573
1038
|
try {
|
|
574
1039
|
if (client.synthesizeChunks) {
|
|
575
|
-
const
|
|
1040
|
+
const normalizedChunks = chunks.map((chunk) => {
|
|
1041
|
+
if (typeof chunk === "string" || chunk.sourceNodePath || !options.sourceNodePath) return chunk;
|
|
1042
|
+
return { ...chunk, sourceNodePath: [...options.sourceNodePath] };
|
|
1043
|
+
});
|
|
1044
|
+
const value = await client.synthesizeChunks(normalizedChunks, {
|
|
1045
|
+
onProgress: options.onProgress,
|
|
1046
|
+
outputFormat: options.outputFormat,
|
|
1047
|
+
signal: options.signal,
|
|
1048
|
+
timeoutMs: options.timeoutMs,
|
|
1049
|
+
sourceNodePath: options.sourceNodePath,
|
|
1050
|
+
concurrency: options.concurrency,
|
|
1051
|
+
retryOptions: options.retryOptions
|
|
1052
|
+
});
|
|
576
1053
|
return { ok: true, success: true, status: "success", value };
|
|
577
1054
|
}
|
|
578
|
-
const results =
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
1055
|
+
const results = new Array(chunks.length);
|
|
1056
|
+
let completed = 0;
|
|
1057
|
+
let nextIndex = 0;
|
|
1058
|
+
const concurrency = resolveConcurrency2(options.concurrency, chunks.length);
|
|
1059
|
+
const worker = async () => {
|
|
1060
|
+
while (true) {
|
|
1061
|
+
const index = nextIndex++;
|
|
1062
|
+
if (index >= chunks.length) return;
|
|
1063
|
+
const chunk = chunks[index];
|
|
1064
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
1065
|
+
const sourceNodePath = input.sourceNodePath;
|
|
1066
|
+
const originalTextRange = input.originalTextRange;
|
|
1067
|
+
pending(index, "synthesizing");
|
|
1068
|
+
const startedAt = Date.now();
|
|
1069
|
+
try {
|
|
1070
|
+
const result = await retryableSynthesis(
|
|
1071
|
+
() => client.synthesizeSsml(input.ssml, {
|
|
1072
|
+
outputFormat: options.outputFormat,
|
|
1073
|
+
signal: options.signal,
|
|
1074
|
+
timeoutMs: options.timeoutMs,
|
|
1075
|
+
sourceNodePath: input.sourceNodePath ?? options.sourceNodePath
|
|
1076
|
+
}),
|
|
1077
|
+
options.retryOptions,
|
|
1078
|
+
options.signal,
|
|
1079
|
+
(retryAttempt, nextRetryDelayMs) => options.onProgress?.({
|
|
1080
|
+
currentChunk: completed,
|
|
1081
|
+
totalChunks: chunks.length,
|
|
1082
|
+
percent: chunks.length === 0 ? 100 : Math.round(completed / chunks.length * 100),
|
|
1083
|
+
chunkIndex: index,
|
|
1084
|
+
originalTextRange: input.originalTextRange,
|
|
1085
|
+
status: "synthesizing",
|
|
1086
|
+
durationMs: Date.now() - startedAt,
|
|
1087
|
+
retryAttempt,
|
|
1088
|
+
nextRetryDelayMs,
|
|
1089
|
+
isRetrying: true
|
|
1090
|
+
})
|
|
1091
|
+
);
|
|
1092
|
+
results[index] = {
|
|
1093
|
+
...result,
|
|
1094
|
+
...input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {},
|
|
1095
|
+
...sourceNodePath ? {
|
|
1096
|
+
boundaries: result.boundaries?.map((event) => ({
|
|
1097
|
+
...event,
|
|
1098
|
+
sourceNodePath: [...sourceNodePath],
|
|
1099
|
+
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
1100
|
+
})),
|
|
1101
|
+
visemes: result.visemes?.map((event) => ({
|
|
1102
|
+
...event,
|
|
1103
|
+
sourceNodePath: [...sourceNodePath],
|
|
1104
|
+
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
1105
|
+
})),
|
|
1106
|
+
bookmarks: result.bookmarks?.map((event) => ({
|
|
1107
|
+
...event,
|
|
1108
|
+
sourceNodePath: [...sourceNodePath],
|
|
1109
|
+
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
1110
|
+
}))
|
|
1111
|
+
} : {},
|
|
1112
|
+
...originalTextRange ? {
|
|
1113
|
+
boundaries: result.boundaries?.map((event) => ({
|
|
1114
|
+
...event,
|
|
1115
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1116
|
+
})),
|
|
1117
|
+
wordBoundary: result.wordBoundary?.map((event) => ({
|
|
1118
|
+
...event,
|
|
1119
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1120
|
+
})),
|
|
1121
|
+
wordBoundaries: result.wordBoundaries?.map((event) => ({
|
|
1122
|
+
...event,
|
|
1123
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1124
|
+
})),
|
|
1125
|
+
visemes: result.visemes?.map((event) => ({
|
|
1126
|
+
...event,
|
|
1127
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1128
|
+
})),
|
|
1129
|
+
bookmarks: result.bookmarks?.map((event) => ({
|
|
1130
|
+
...event,
|
|
1131
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1132
|
+
}))
|
|
1133
|
+
} : {}
|
|
1134
|
+
};
|
|
1135
|
+
completed += 1;
|
|
1136
|
+
options.onProgress?.({
|
|
1137
|
+
currentChunk: completed,
|
|
1138
|
+
totalChunks: chunks.length,
|
|
1139
|
+
percent: chunks.length === 0 ? 100 : Math.round(completed / chunks.length * 100),
|
|
1140
|
+
chunkIndex: index,
|
|
1141
|
+
originalTextRange: input.originalTextRange,
|
|
1142
|
+
status: "success",
|
|
1143
|
+
durationMs: Date.now() - startedAt
|
|
1144
|
+
});
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
options.onProgress?.({
|
|
1147
|
+
currentChunk: completed,
|
|
1148
|
+
totalChunks: chunks.length,
|
|
1149
|
+
percent: chunks.length === 0 ? 100 : Math.round(index / chunks.length * 100),
|
|
1150
|
+
chunkIndex: index,
|
|
1151
|
+
originalTextRange: input.originalTextRange,
|
|
1152
|
+
status: "failed",
|
|
1153
|
+
durationMs: Date.now() - startedAt,
|
|
1154
|
+
error
|
|
1155
|
+
});
|
|
1156
|
+
throw error;
|
|
1157
|
+
}
|
|
619
1158
|
}
|
|
620
|
-
}
|
|
1159
|
+
};
|
|
1160
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
1161
|
+
const orderedResults = results.filter((result) => result !== void 0);
|
|
621
1162
|
return {
|
|
622
1163
|
ok: true,
|
|
623
1164
|
success: true,
|
|
624
1165
|
status: "success",
|
|
625
|
-
value: mergeSynthesisResults(
|
|
1166
|
+
value: mergeSynthesisResults(orderedResults, {
|
|
1167
|
+
format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3",
|
|
1168
|
+
signal: options.signal
|
|
1169
|
+
})
|
|
626
1170
|
};
|
|
627
1171
|
} catch (error) {
|
|
628
|
-
const
|
|
629
|
-
return
|
|
1172
|
+
const synthesisError = toSynthesisError(error);
|
|
1173
|
+
return failure(synthesisError);
|
|
630
1174
|
}
|
|
631
1175
|
}
|
|
1176
|
+
function withValidationSignal(options, signal) {
|
|
1177
|
+
if (!signal) return options;
|
|
1178
|
+
return {
|
|
1179
|
+
...options,
|
|
1180
|
+
urlValidatorSignal: signal,
|
|
1181
|
+
urlValidation: { ...options.urlValidation ?? {}, signal }
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
632
1184
|
|
|
633
1185
|
// src/client.ts
|
|
634
1186
|
var ENDPOINT_TEMPLATE = "https://{region}.tts.speech.microsoft.com/cognitiveservices/v1";
|
|
@@ -645,11 +1197,21 @@ var AzureTtsClient = class {
|
|
|
645
1197
|
const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs };
|
|
646
1198
|
return synthesizeSpeech(ssml, config);
|
|
647
1199
|
}
|
|
648
|
-
async synthesizeSsml(ssml) {
|
|
1200
|
+
async synthesizeSsml(ssml, options = {}) {
|
|
649
1201
|
const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
|
|
650
1202
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
651
1203
|
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
652
|
-
return synthesizeSsml(ssml, {
|
|
1204
|
+
return synthesizeSsml(ssml, {
|
|
1205
|
+
endpoint,
|
|
1206
|
+
region,
|
|
1207
|
+
subscriptionKey,
|
|
1208
|
+
outputFormat: options.outputFormat ?? outputFormat,
|
|
1209
|
+
signal: options.signal ?? signal,
|
|
1210
|
+
timeoutMs: options.timeoutMs ?? timeoutMs,
|
|
1211
|
+
sourceNodePath: options.sourceNodePath,
|
|
1212
|
+
sourceTextSegments: options.sourceTextSegments,
|
|
1213
|
+
sourceMarkers: options.sourceMarkers
|
|
1214
|
+
});
|
|
653
1215
|
}
|
|
654
1216
|
async synthesizeChunks(chunks, options = {}) {
|
|
655
1217
|
const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
|
|
@@ -658,10 +1220,13 @@ var AzureTtsClient = class {
|
|
|
658
1220
|
endpoint,
|
|
659
1221
|
region,
|
|
660
1222
|
subscriptionKey,
|
|
661
|
-
outputFormat,
|
|
662
|
-
signal,
|
|
663
|
-
timeoutMs,
|
|
664
|
-
|
|
1223
|
+
outputFormat: options.outputFormat ?? outputFormat,
|
|
1224
|
+
signal: options.signal ?? signal,
|
|
1225
|
+
timeoutMs: options.timeoutMs ?? timeoutMs,
|
|
1226
|
+
sourceNodePath: options.sourceNodePath,
|
|
1227
|
+
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
1228
|
+
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
1229
|
+
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions
|
|
665
1230
|
});
|
|
666
1231
|
}
|
|
667
1232
|
async synthesizeSsmlSafe(ssml, options = {}) {
|
|
@@ -671,7 +1236,11 @@ var AzureTtsClient = class {
|
|
|
671
1236
|
return synthesizeSsmlChunksSafe(this, chunks, {
|
|
672
1237
|
...options,
|
|
673
1238
|
outputFormat: options.outputFormat ?? __privateGet(this, _options).outputFormat,
|
|
674
|
-
|
|
1239
|
+
signal: options.signal ?? __privateGet(this, _options).signal,
|
|
1240
|
+
timeoutMs: options.timeoutMs ?? __privateGet(this, _options).timeoutMs,
|
|
1241
|
+
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
1242
|
+
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
1243
|
+
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions
|
|
675
1244
|
});
|
|
676
1245
|
}
|
|
677
1246
|
async synthesizeSsmlChunksSafe(chunks, options = {}) {
|
|
@@ -767,16 +1336,23 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
767
1336
|
};
|
|
768
1337
|
}
|
|
769
1338
|
export {
|
|
1339
|
+
AudioFormatMismatchError,
|
|
770
1340
|
AzureTtsClient,
|
|
771
1341
|
AzureTtsError,
|
|
772
1342
|
AzureTtsSdkError,
|
|
773
1343
|
ChunkValidationError,
|
|
1344
|
+
DEFAULT_OUTPUT_FORMAT,
|
|
1345
|
+
MergeError,
|
|
1346
|
+
SynthesisCancelledError,
|
|
1347
|
+
SynthesisTimeoutError,
|
|
774
1348
|
UnsupportedMergeFormatError,
|
|
775
1349
|
canMergeAudioFormat,
|
|
776
1350
|
fetchAzureVoiceCatalog,
|
|
1351
|
+
inspectAudioSpecification,
|
|
777
1352
|
mergeAudioBuffers,
|
|
778
1353
|
mergeSynthesisResults,
|
|
779
1354
|
resolveMergeAudioFormat,
|
|
1355
|
+
resolveMimeType,
|
|
780
1356
|
synthesizeSpeech,
|
|
781
1357
|
synthesizeSsml,
|
|
782
1358
|
synthesizeSsmlChunks,
|