@ssml-builder-js/azure-tts-client 2.15.0 → 2.17.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 +121 -7
- package/dist/index.d.ts +121 -7
- package/dist/index.js +786 -170
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +786 -171
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +17 -4
- package/src/errors.ts +66 -2
- package/src/index.ts +26 -2
- package/src/safe.ts +414 -118
- package/src/synthesis.ts +514 -69
- package/src/types.ts +89 -0
- package/src/voiceCatalog.ts +4 -0
- package/test/v216-pipeline.test.ts +110 -0
- package/test/v217-pipeline.test.ts +125 -0
package/dist/index.js
CHANGED
|
@@ -37,9 +37,11 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
|
|
|
37
37
|
// src/index.ts
|
|
38
38
|
var index_exports = {};
|
|
39
39
|
__export(index_exports, {
|
|
40
|
+
AudioFormatMismatchError: () => AudioFormatMismatchError,
|
|
40
41
|
AzureTtsClient: () => AzureTtsClient,
|
|
41
42
|
AzureTtsError: () => AzureTtsError,
|
|
42
43
|
AzureTtsSdkError: () => AzureTtsSdkError,
|
|
44
|
+
BatchChunkValidationError: () => BatchChunkValidationError,
|
|
43
45
|
ChunkValidationError: () => ChunkValidationError,
|
|
44
46
|
DEFAULT_OUTPUT_FORMAT: () => DEFAULT_OUTPUT_FORMAT,
|
|
45
47
|
MergeError: () => MergeError,
|
|
@@ -48,6 +50,8 @@ __export(index_exports, {
|
|
|
48
50
|
UnsupportedMergeFormatError: () => UnsupportedMergeFormatError,
|
|
49
51
|
canMergeAudioFormat: () => canMergeAudioFormat,
|
|
50
52
|
fetchAzureVoiceCatalog: () => fetchAzureVoiceCatalog,
|
|
53
|
+
getRetryAfterDelayMs: () => getRetryAfterDelayMs,
|
|
54
|
+
inspectAudioSpecification: () => inspectAudioSpecification,
|
|
51
55
|
mergeAudioBuffers: () => mergeAudioBuffers,
|
|
52
56
|
mergeSynthesisResults: () => mergeSynthesisResults,
|
|
53
57
|
resolveMergeAudioFormat: () => resolveMergeAudioFormat,
|
|
@@ -62,7 +66,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
62
66
|
|
|
63
67
|
// src/errors.ts
|
|
64
68
|
var AzureTtsError = class extends Error {
|
|
65
|
-
constructor(status, statusText, responseBody, requestId) {
|
|
69
|
+
constructor(status, statusText, responseBody, requestId, responseHeaders) {
|
|
66
70
|
super(`Azure TTS request failed: ${status} ${statusText}`);
|
|
67
71
|
this.kind = "azure-api-error";
|
|
68
72
|
this.name = "AzureTtsError";
|
|
@@ -70,8 +74,37 @@ var AzureTtsError = class extends Error {
|
|
|
70
74
|
this.statusText = statusText;
|
|
71
75
|
this.responseBody = responseBody;
|
|
72
76
|
this.requestId = requestId;
|
|
77
|
+
const value = responseHeaders instanceof Headers ? responseHeaders.get("retry-after") : responseHeaders?.["retry-after"] ?? responseHeaders?.["Retry-After"];
|
|
78
|
+
const seconds = value ? Number(value.trim()) : NaN;
|
|
79
|
+
const date = value ? Date.parse(value) : NaN;
|
|
80
|
+
if (Number.isFinite(seconds) && seconds >= 0) this.retryAfterMs = seconds * 1e3;
|
|
81
|
+
else if (Number.isFinite(date)) this.retryAfterMs = Math.max(0, date - Date.now());
|
|
73
82
|
}
|
|
74
83
|
};
|
|
84
|
+
function getRetryAfterDelayMs(error) {
|
|
85
|
+
if (error instanceof AzureTtsError && error.retryAfterMs !== void 0) return error.retryAfterMs;
|
|
86
|
+
if (!error || typeof error !== "object") return void 0;
|
|
87
|
+
const candidate = error;
|
|
88
|
+
if (typeof candidate.retryAfterMs === "number" && candidate.retryAfterMs >= 0) return candidate.retryAfterMs;
|
|
89
|
+
const headers = candidate.headers ?? candidate.response?.headers;
|
|
90
|
+
if (headers instanceof Headers) {
|
|
91
|
+
const value = headers.get("retry-after");
|
|
92
|
+
if (!value) return void 0;
|
|
93
|
+
const seconds = Number(value.trim());
|
|
94
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
|
|
95
|
+
const date = Date.parse(value);
|
|
96
|
+
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : void 0;
|
|
97
|
+
}
|
|
98
|
+
if (headers && typeof headers === "object") {
|
|
99
|
+
const value = headers["retry-after"] ?? headers["Retry-After"];
|
|
100
|
+
if (typeof value !== "string") return void 0;
|
|
101
|
+
const seconds = Number(value.trim());
|
|
102
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
|
|
103
|
+
const date = Date.parse(value);
|
|
104
|
+
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : void 0;
|
|
105
|
+
}
|
|
106
|
+
return void 0;
|
|
107
|
+
}
|
|
75
108
|
var AzureTtsSdkError = class extends AzureTtsError {
|
|
76
109
|
constructor(errorDetails) {
|
|
77
110
|
super(0, "Speech SDK", errorDetails, null);
|
|
@@ -102,6 +135,14 @@ var MergeError = class extends Error {
|
|
|
102
135
|
this.cause = cause;
|
|
103
136
|
}
|
|
104
137
|
};
|
|
138
|
+
var AudioFormatMismatchError = class extends Error {
|
|
139
|
+
constructor(message, inputSpecs = []) {
|
|
140
|
+
super(message);
|
|
141
|
+
this.kind = "audio-format-mismatch";
|
|
142
|
+
this.name = "AudioFormatMismatchError";
|
|
143
|
+
this.inputSpecs = inputSpecs;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
105
146
|
var UnsupportedMergeFormatError = class extends Error {
|
|
106
147
|
constructor(format) {
|
|
107
148
|
super(`Audio format "${format}" cannot be safely concatenated; container re-multiplexing is required.`);
|
|
@@ -111,7 +152,7 @@ var UnsupportedMergeFormatError = class extends Error {
|
|
|
111
152
|
}
|
|
112
153
|
};
|
|
113
154
|
function toSynthesisError(error) {
|
|
114
|
-
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
|
|
155
|
+
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof AudioFormatMismatchError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
|
|
115
156
|
return error;
|
|
116
157
|
const message = error instanceof Error ? error.message : String(error);
|
|
117
158
|
if (/cancel|abort/i.test(message)) return new SynthesisCancelledError(message);
|
|
@@ -241,6 +282,115 @@ function parseWav(buffer) {
|
|
|
241
282
|
}
|
|
242
283
|
return { chunks, data, format };
|
|
243
284
|
}
|
|
285
|
+
function formatNumber(format, pattern, fallback) {
|
|
286
|
+
const match = pattern.exec(format);
|
|
287
|
+
return match?.[1] ? Number(match[1]) : fallback;
|
|
288
|
+
}
|
|
289
|
+
function formatChannels(format, fallback) {
|
|
290
|
+
if (/stereo|2ch|dual/i.test(format)) return 2;
|
|
291
|
+
if (/mono|1ch/i.test(format)) return 1;
|
|
292
|
+
return fallback;
|
|
293
|
+
}
|
|
294
|
+
function formatAudioSpecification(format) {
|
|
295
|
+
const sampleRate = formatNumber(format, /(\d+)(?:khz|kHz|hz|Hz)/, 0);
|
|
296
|
+
const channels = formatChannels(format, 0);
|
|
297
|
+
const bitrateMatch = /(\d+)(?:kbitrate|kbps|kbits?)/i.exec(format);
|
|
298
|
+
const bitrate = bitrateMatch?.[1] ? Number(bitrateMatch[1]) * 1e3 : void 0;
|
|
299
|
+
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";
|
|
300
|
+
const bitDepthMatch = /(\d+)bit/i.exec(format);
|
|
301
|
+
const container = /(?:wav|wave|riff)/i.test(format) ? "riff-wave" : /mp3|mpeg/i.test(format) ? "mp3-raw" : /ogg/i.test(format) ? "ogg" : /webm/i.test(format) ? "webm" : /raw/i.test(format) ? "raw" : void 0;
|
|
302
|
+
return {
|
|
303
|
+
format,
|
|
304
|
+
mimeType: resolveMimeType(format),
|
|
305
|
+
codec,
|
|
306
|
+
sampleRate,
|
|
307
|
+
channels,
|
|
308
|
+
...bitrate ? { bitrate } : {},
|
|
309
|
+
...bitDepthMatch?.[1] ? { bitDepth: Number(bitDepthMatch[1]) } : {},
|
|
310
|
+
...container ? { container } : {},
|
|
311
|
+
isVbr: /vbr/i.test(format),
|
|
312
|
+
isCompressed: codec === "mp3" || codec === "opus" || codec === "silk"
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function parseMp3Specification(buffer, format) {
|
|
316
|
+
const bytes = stripMp3Tags(buffer);
|
|
317
|
+
const bitrates = [
|
|
318
|
+
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
319
|
+
[0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0],
|
|
320
|
+
[0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0]
|
|
321
|
+
];
|
|
322
|
+
const sampleRates = [
|
|
323
|
+
[44100, 48e3, 32e3],
|
|
324
|
+
[22050, 24e3, 16e3],
|
|
325
|
+
[11025, 12e3, 8e3]
|
|
326
|
+
];
|
|
327
|
+
for (let index = 0; index + 4 <= bytes.length; index += 1) {
|
|
328
|
+
if (bytes[index] !== 255 || (bytes[index + 1] ?? 0) < 224) continue;
|
|
329
|
+
const header = bytes[index + 1] ?? 0;
|
|
330
|
+
const versionBits = header >> 3 & 3;
|
|
331
|
+
const layer = header >> 1 & 3;
|
|
332
|
+
const bitrateIndex = (bytes[index + 2] ?? 0) >> 4;
|
|
333
|
+
const sampleIndex = (bytes[index + 2] ?? 0) >> 2 & 3;
|
|
334
|
+
if (versionBits === 1 || layer !== 1 || bitrateIndex === 0 || bitrateIndex === 15 || sampleIndex === 3) continue;
|
|
335
|
+
const versionIndex = versionBits === 3 ? 0 : versionBits === 2 ? 1 : 2;
|
|
336
|
+
const bitrateTable = versionBits === 3 ? bitrates[1] : bitrates[2];
|
|
337
|
+
const sampleRate = sampleRates[versionIndex]?.[sampleIndex] ?? 0;
|
|
338
|
+
const bitrateKbps = bitrateTable?.[bitrateIndex] ?? 0;
|
|
339
|
+
if (!sampleRate || !bitrateKbps) continue;
|
|
340
|
+
return {
|
|
341
|
+
format,
|
|
342
|
+
mimeType: "audio/mpeg",
|
|
343
|
+
codec: "mp3",
|
|
344
|
+
sampleRate,
|
|
345
|
+
channels: (bytes[index + 3] ?? 0) >> 6 === 3 ? 1 : 2,
|
|
346
|
+
bitrate: bitrateKbps * 1e3,
|
|
347
|
+
container: "mp3-raw",
|
|
348
|
+
isVbr: false,
|
|
349
|
+
isCompressed: true
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
return void 0;
|
|
353
|
+
}
|
|
354
|
+
function inspectAudioSpecification(buffer, format) {
|
|
355
|
+
if (isWavFormat(format) || ascii(new Uint8Array(buffer), 0, "RIFF")) {
|
|
356
|
+
const parsed = parseWav(buffer);
|
|
357
|
+
if (parsed.format.byteLength < 16) throw new Error("Invalid WAV fmt chunk.");
|
|
358
|
+
const view = new DataView(parsed.format.buffer, parsed.format.byteOffset, parsed.format.byteLength);
|
|
359
|
+
const sampleRate = view.getUint32(4, true);
|
|
360
|
+
const channels = view.getUint16(2, true);
|
|
361
|
+
const bitsPerSample = parsed.format.byteLength >= 16 ? view.getUint16(14, true) : 0;
|
|
362
|
+
const formatCode = view.getUint16(0, true);
|
|
363
|
+
return {
|
|
364
|
+
format,
|
|
365
|
+
mimeType: "audio/wav",
|
|
366
|
+
codec: formatCode === 1 ? "pcm" : "unknown",
|
|
367
|
+
sampleRate,
|
|
368
|
+
channels,
|
|
369
|
+
...sampleRate && channels && bitsPerSample ? { bitrate: sampleRate * channels * bitsPerSample } : {},
|
|
370
|
+
bitDepth: bitsPerSample,
|
|
371
|
+
container: "riff-wave",
|
|
372
|
+
isVbr: false,
|
|
373
|
+
isCompressed: formatCode !== 1
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
if (isMp3Format(format)) return parseMp3Specification(buffer, format) ?? formatAudioSpecification(format);
|
|
377
|
+
return formatAudioSpecification(format);
|
|
378
|
+
}
|
|
379
|
+
function validateAudioSpecifications(specs) {
|
|
380
|
+
const first = specs[0];
|
|
381
|
+
if (!first) return;
|
|
382
|
+
const mismatch = specs.find(
|
|
383
|
+
(spec) => spec.sampleRate !== first.sampleRate || spec.channels !== first.channels || first.bitrate !== void 0 && spec.bitrate !== void 0 && spec.bitrate !== first.bitrate || first.bitDepth !== void 0 && spec.bitDepth !== void 0 && spec.bitDepth !== first.bitDepth || first.container !== void 0 && spec.container !== void 0 && spec.container !== first.container || first.isVbr !== void 0 && spec.isVbr !== void 0 && spec.isVbr !== first.isVbr
|
|
384
|
+
);
|
|
385
|
+
if (mismatch)
|
|
386
|
+
throw new AudioFormatMismatchError(
|
|
387
|
+
`Audio chunks have incompatible specifications: ${first.sampleRate}Hz/${first.channels}ch versus ${mismatch.sampleRate}Hz/${mismatch.channels}ch.`,
|
|
388
|
+
specs
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
function isAudioFormatMismatch(error) {
|
|
392
|
+
return error instanceof AudioFormatMismatchError || error !== null && typeof error === "object" && "kind" in error && error.kind === "audio-format-mismatch";
|
|
393
|
+
}
|
|
244
394
|
function writeUint32(target, offset, value) {
|
|
245
395
|
new DataView(target.buffer).setUint32(offset, value, true);
|
|
246
396
|
}
|
|
@@ -326,6 +476,7 @@ function mergeAudioBuffers(buffers, options) {
|
|
|
326
476
|
const format = typeof options === "string" ? options : options?.format;
|
|
327
477
|
if (!format) throw new UnsupportedMergeFormatError("");
|
|
328
478
|
try {
|
|
479
|
+
validateAudioSpecifications(buffers.map((buffer) => inspectAudioSpecification(buffer, format)));
|
|
329
480
|
if (isWavFormat(format)) return mergeWavBuffers(buffers);
|
|
330
481
|
if (isMp3Format(format)) {
|
|
331
482
|
const parts = buffers.map(stripMp3Tags);
|
|
@@ -348,7 +499,8 @@ function mergeAudioBuffers(buffers, options) {
|
|
|
348
499
|
}
|
|
349
500
|
throw new UnsupportedMergeFormatError(format);
|
|
350
501
|
} catch (error) {
|
|
351
|
-
if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError)
|
|
502
|
+
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
503
|
+
throw error;
|
|
352
504
|
throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
|
|
353
505
|
}
|
|
354
506
|
}
|
|
@@ -426,17 +578,24 @@ async function synthesizeSsml(ssml, config) {
|
|
|
426
578
|
return {
|
|
427
579
|
originalTextRange: { ...marker.originalTextRange },
|
|
428
580
|
sourceNodePath: [...marker.sourceNodePath],
|
|
429
|
-
textRange: { ...marker.originalTextRange }
|
|
581
|
+
textRange: { ...marker.originalTextRange },
|
|
582
|
+
mappingStatus: "exact"
|
|
430
583
|
};
|
|
431
584
|
}
|
|
432
|
-
if (sourceSegments.length === 0 && !config.sourceTextRange && !config.sourceNodePath)
|
|
585
|
+
if (sourceSegments.length === 0 && !config.sourceTextRange && !config.sourceNodePath) {
|
|
586
|
+
return { mappingStatus: "unmapped" };
|
|
587
|
+
}
|
|
433
588
|
const value = text ?? "";
|
|
434
589
|
let localStart = Number.isFinite(offsetHint) && (offsetHint ?? 0) >= 0 ? offsetHint : -1;
|
|
435
|
-
|
|
590
|
+
let mappingStatus = "exact";
|
|
591
|
+
if (value && localStart >= 0 && sourceText.slice(localStart, localStart + value.length) !== value) {
|
|
436
592
|
localStart = -1;
|
|
593
|
+
mappingStatus = "fallback";
|
|
594
|
+
}
|
|
437
595
|
if (localStart < 0 || localStart > sourceText.length) {
|
|
438
596
|
localStart = value ? sourceText.indexOf(value, sourceEventCursor) : sourceEventCursor;
|
|
439
597
|
if (localStart < 0) localStart = value ? sourceText.indexOf(value) : sourceEventCursor;
|
|
598
|
+
mappingStatus = "fallback";
|
|
440
599
|
}
|
|
441
600
|
localStart = Math.max(0, localStart);
|
|
442
601
|
const localEnd = Math.min(sourceText.length, localStart + value.length);
|
|
@@ -447,7 +606,8 @@ async function synthesizeSsml(ssml, config) {
|
|
|
447
606
|
return {
|
|
448
607
|
originalTextRange: { ...fallbackRange },
|
|
449
608
|
textRange: { ...fallbackRange },
|
|
450
|
-
...segment ? { sourceNodePath: [...segment.sourceNodePath] } : config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {}
|
|
609
|
+
...segment ? { sourceNodePath: [...segment.sourceNodePath] } : config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
|
|
610
|
+
mappingStatus: segment || config.sourceTextRange || config.sourceNodePath ? mappingStatus : "unmapped"
|
|
451
611
|
};
|
|
452
612
|
};
|
|
453
613
|
synthesizer.wordBoundary = (_sender, event) => {
|
|
@@ -496,20 +656,32 @@ async function synthesizeSsml(ssml, config) {
|
|
|
496
656
|
);
|
|
497
657
|
const durationMs = result.audioDuration ? ticksToMilliseconds(result.audioDuration) : eventDurationMs;
|
|
498
658
|
const requestId = result.resultId;
|
|
499
|
-
const addSourceMetadata = (event) =>
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
659
|
+
const addSourceMetadata = (event) => {
|
|
660
|
+
const mapped = {
|
|
661
|
+
...event,
|
|
662
|
+
...config.sourceTextRange && !("textRange" in event) ? { textRange: { ...config.sourceTextRange } } : {},
|
|
663
|
+
...config.sourceTextRange && !("originalTextRange" in event) ? { originalTextRange: { ...config.sourceTextRange } } : {},
|
|
664
|
+
...config.chunkIndex !== void 0 ? { chunkIndex: config.chunkIndex } : {},
|
|
665
|
+
...config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
|
|
666
|
+
...requestId ? { requestId } : {}
|
|
667
|
+
};
|
|
668
|
+
if (event.mappingStatus === "unmapped") {
|
|
669
|
+
Object.defineProperty(mapped, "mappingStatus", { value: "unmapped", enumerable: false });
|
|
670
|
+
Object.defineProperty(mapped, "toJSON", {
|
|
671
|
+
value: () => ({ ...mapped, mappingStatus: "unmapped" }),
|
|
672
|
+
enumerable: false
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
return mapped;
|
|
676
|
+
};
|
|
507
677
|
const sourceBoundaries = boundaries.map((boundary) => addSourceMetadata(boundary));
|
|
508
678
|
const sourceVisemes = visemes.map((viseme) => addSourceMetadata(viseme));
|
|
509
679
|
const sourceBookmarks = bookmarks.map((bookmark) => addSourceMetadata(bookmark));
|
|
510
680
|
resolve({
|
|
511
681
|
audioData: result.audioData,
|
|
512
682
|
durationMs,
|
|
683
|
+
audioSpec: inspectAudioSpecification(result.audioData, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
|
|
684
|
+
mimeType: resolveMimeType(config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
|
|
513
685
|
...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
|
|
514
686
|
...requestId ? { requestId } : {},
|
|
515
687
|
...sourceBoundaries.length > 0 ? { boundaries: sourceBoundaries, wordBoundary: sourceBoundaries, wordBoundaries: sourceBoundaries } : {},
|
|
@@ -522,10 +694,11 @@ async function synthesizeSsml(ssml, config) {
|
|
|
522
694
|
abortHandler = () => rejectWithError(new SynthesisCancelledError());
|
|
523
695
|
config.signal.addEventListener("abort", abortHandler, { once: true });
|
|
524
696
|
}
|
|
525
|
-
|
|
697
|
+
const timeoutMs = config.timeouts?.perChunkMs ?? config.timeoutMs;
|
|
698
|
+
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
526
699
|
timeout = setTimeout(
|
|
527
|
-
() => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${
|
|
528
|
-
|
|
700
|
+
() => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`)),
|
|
701
|
+
timeoutMs
|
|
529
702
|
);
|
|
530
703
|
}
|
|
531
704
|
synthesizer.speakSsmlAsync(ssml, cb, rejectWithError);
|
|
@@ -534,9 +707,109 @@ async function synthesizeSsml(ssml, config) {
|
|
|
534
707
|
}
|
|
535
708
|
});
|
|
536
709
|
}
|
|
710
|
+
function isRetryableSynthesisError(error) {
|
|
711
|
+
if (error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError) return false;
|
|
712
|
+
if (error instanceof AzureTtsError && error.status !== 0)
|
|
713
|
+
return error.status === 429 || error.status >= 500 && error.status < 600;
|
|
714
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
715
|
+
if (/\b4\d{2}\b/.test(message)) return false;
|
|
716
|
+
const status = error && typeof error === "object" && "status" in error ? error.status : void 0;
|
|
717
|
+
if (typeof status === "number") return status === 429 || status >= 500 && status < 600;
|
|
718
|
+
return /network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message);
|
|
719
|
+
}
|
|
720
|
+
function retryDelay(options, retryAttempt, error) {
|
|
721
|
+
const retryAfterMs = getRetryAfterDelayMs(error);
|
|
722
|
+
if (retryAfterMs !== void 0) return retryAfterMs;
|
|
723
|
+
const base = Math.min(options.maxDelayMs, options.initialDelayMs * 2 ** Math.max(0, retryAttempt - 1));
|
|
724
|
+
return Math.floor(Math.random() * (base + 1));
|
|
725
|
+
}
|
|
726
|
+
function resolveConcurrency(value, total) {
|
|
727
|
+
if (value === void 0) return 1;
|
|
728
|
+
if (value === Infinity) return Math.max(1, total);
|
|
729
|
+
return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
|
|
730
|
+
}
|
|
731
|
+
async function waitForRetry(delayMs, signal) {
|
|
732
|
+
if (signal?.aborted) throw new SynthesisCancelledError();
|
|
733
|
+
if (delayMs <= 0) return;
|
|
734
|
+
await new Promise((resolve, reject) => {
|
|
735
|
+
let timer;
|
|
736
|
+
const abort = () => {
|
|
737
|
+
clearTimeout(timer);
|
|
738
|
+
signal?.removeEventListener("abort", abort);
|
|
739
|
+
reject(new SynthesisCancelledError());
|
|
740
|
+
};
|
|
741
|
+
timer = setTimeout(() => {
|
|
742
|
+
signal?.removeEventListener("abort", abort);
|
|
743
|
+
resolve();
|
|
744
|
+
}, delayMs);
|
|
745
|
+
if (signal) {
|
|
746
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
async function synthesizeWithRetry(ssml, config, retryOptions, onRetry) {
|
|
751
|
+
const options = retryOptions ? {
|
|
752
|
+
maxRetries: Math.max(0, Math.floor(retryOptions.maxRetries)),
|
|
753
|
+
initialDelayMs: Math.max(0, retryOptions.initialDelayMs),
|
|
754
|
+
maxDelayMs: Math.max(0, retryOptions.maxDelayMs),
|
|
755
|
+
shouldRetry: retryOptions.shouldRetry
|
|
756
|
+
} : void 0;
|
|
757
|
+
let attempt = 0;
|
|
758
|
+
while (true) {
|
|
759
|
+
if (config.signal?.aborted) throw new SynthesisCancelledError();
|
|
760
|
+
try {
|
|
761
|
+
return await synthesizeSsml(ssml, config);
|
|
762
|
+
} catch (error) {
|
|
763
|
+
if (!options || attempt >= options.maxRetries || !(options.shouldRetry?.(error, attempt + 1) ?? isRetryableSynthesisError(error)))
|
|
764
|
+
throw error;
|
|
765
|
+
attempt += 1;
|
|
766
|
+
const delayMs = retryDelay(options, attempt, error);
|
|
767
|
+
onRetry(attempt, delayMs);
|
|
768
|
+
await waitForRetry(delayMs, config.signal);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
function createAbortScope(parent, timeoutMs) {
|
|
773
|
+
const controller = new AbortController();
|
|
774
|
+
let didTimeout = false;
|
|
775
|
+
const onAbort = () => controller.abort();
|
|
776
|
+
if (parent?.aborted) controller.abort();
|
|
777
|
+
parent?.addEventListener("abort", onAbort, { once: true });
|
|
778
|
+
const timer = timeoutMs !== void 0 && timeoutMs > 0 ? setTimeout(() => {
|
|
779
|
+
didTimeout = true;
|
|
780
|
+
controller.abort();
|
|
781
|
+
}, timeoutMs) : void 0;
|
|
782
|
+
return {
|
|
783
|
+
signal: controller.signal,
|
|
784
|
+
timedOut: () => didTimeout,
|
|
785
|
+
dispose: () => {
|
|
786
|
+
if (timer) clearTimeout(timer);
|
|
787
|
+
parent?.removeEventListener("abort", onAbort);
|
|
788
|
+
},
|
|
789
|
+
abort: () => controller.abort()
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs, onRetry) {
|
|
793
|
+
const scope = createAbortScope(config.signal, timeoutMs);
|
|
794
|
+
try {
|
|
795
|
+
return await synthesizeWithRetry(ssml, { ...config, signal: scope.signal }, retryOptions, onRetry);
|
|
796
|
+
} catch (error) {
|
|
797
|
+
if (scope.timedOut()) throw new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`);
|
|
798
|
+
throw error;
|
|
799
|
+
} finally {
|
|
800
|
+
scope.dispose();
|
|
801
|
+
}
|
|
802
|
+
}
|
|
537
803
|
async function synthesizeSsmlChunks(chunks, config) {
|
|
538
|
-
const results =
|
|
804
|
+
const results = new Array(chunks.length);
|
|
539
805
|
const totalChunks = chunks.length;
|
|
806
|
+
const cachedChunks = new Map((config.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
|
|
807
|
+
for (const [index, cached] of cachedChunks) {
|
|
808
|
+
if (index >= 0 && index < totalChunks) results[index] = cached;
|
|
809
|
+
}
|
|
810
|
+
const requestedIndices = config.resumeChunkIndices ? new Set(config.resumeChunkIndices.filter((index) => index >= 0 && index < totalChunks)) : void 0;
|
|
811
|
+
const shouldSynthesize = (index) => !cachedChunks.has(index) && (requestedIndices === void 0 || requestedIndices.has(index));
|
|
812
|
+
const scope = createAbortScope(config.signal, config.timeouts?.totalJobMs);
|
|
540
813
|
const report = (event) => config.onProgress?.(event);
|
|
541
814
|
for (const [index, chunk] of chunks.entries()) {
|
|
542
815
|
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
@@ -550,57 +823,112 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
550
823
|
durationMs: 0
|
|
551
824
|
});
|
|
552
825
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
const result = await synthesizeSsml(input.ssml, {
|
|
567
|
-
...config,
|
|
568
|
-
...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
|
|
569
|
-
...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
|
|
570
|
-
...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
|
|
571
|
-
...input.sourceMarkers ? { sourceMarkers: input.sourceMarkers } : {},
|
|
572
|
-
chunkIndex: index,
|
|
573
|
-
onProgress: void 0
|
|
574
|
-
});
|
|
575
|
-
results.push(result);
|
|
576
|
-
report({
|
|
577
|
-
currentChunk: index + 1,
|
|
578
|
-
totalChunks,
|
|
579
|
-
percent: totalChunks === 0 ? 100 : Math.round((index + 1) / totalChunks * 100),
|
|
580
|
-
chunkIndex: index,
|
|
581
|
-
originalTextRange: input.originalTextRange,
|
|
582
|
-
status: "success",
|
|
583
|
-
durationMs: Date.now() - startedAt
|
|
584
|
-
});
|
|
585
|
-
} catch (error) {
|
|
826
|
+
let completed = [...results].filter((result) => result !== void 0).length;
|
|
827
|
+
let nextIndex = 0;
|
|
828
|
+
const concurrency = resolveConcurrency(config.concurrency, chunks.length);
|
|
829
|
+
let firstError;
|
|
830
|
+
const failedIndices = /* @__PURE__ */ new Set();
|
|
831
|
+
const worker = async () => {
|
|
832
|
+
while (true) {
|
|
833
|
+
const index = nextIndex++;
|
|
834
|
+
if (index >= chunks.length) return;
|
|
835
|
+
if (!shouldSynthesize(index)) continue;
|
|
836
|
+
if (firstError && config.cancelOnFailure !== false) return;
|
|
837
|
+
const chunk = chunks[index];
|
|
838
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
586
839
|
report({
|
|
587
|
-
currentChunk:
|
|
840
|
+
currentChunk: completed,
|
|
588
841
|
totalChunks,
|
|
589
|
-
percent: totalChunks === 0 ? 100 : Math.round(
|
|
842
|
+
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
590
843
|
chunkIndex: index,
|
|
591
844
|
originalTextRange: input.originalTextRange,
|
|
592
|
-
status: "
|
|
593
|
-
durationMs:
|
|
594
|
-
error
|
|
845
|
+
status: "synthesizing",
|
|
846
|
+
durationMs: 0
|
|
595
847
|
});
|
|
596
|
-
|
|
848
|
+
const startedAt = Date.now();
|
|
849
|
+
try {
|
|
850
|
+
const result = await synthesizeChunkWithTimeout(
|
|
851
|
+
input.ssml,
|
|
852
|
+
{
|
|
853
|
+
...config,
|
|
854
|
+
signal: scope.signal,
|
|
855
|
+
...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
|
|
856
|
+
...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
|
|
857
|
+
...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
|
|
858
|
+
...input.sourceMarkers ? { sourceMarkers: input.sourceMarkers } : {},
|
|
859
|
+
chunkIndex: index,
|
|
860
|
+
onProgress: void 0
|
|
861
|
+
},
|
|
862
|
+
config.retryOptions,
|
|
863
|
+
config.timeouts?.chunkWithRetriesMs ?? config.timeouts?.perChunkMs ?? config.timeoutMs,
|
|
864
|
+
(retryAttempt, nextRetryDelayMs) => report({
|
|
865
|
+
currentChunk: completed,
|
|
866
|
+
totalChunks,
|
|
867
|
+
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
868
|
+
chunkIndex: index,
|
|
869
|
+
originalTextRange: input.originalTextRange,
|
|
870
|
+
status: "synthesizing",
|
|
871
|
+
durationMs: Date.now() - startedAt,
|
|
872
|
+
retryAttempt,
|
|
873
|
+
nextRetryDelayMs,
|
|
874
|
+
isRetrying: true
|
|
875
|
+
})
|
|
876
|
+
);
|
|
877
|
+
results[index] = result;
|
|
878
|
+
completed += 1;
|
|
879
|
+
report({
|
|
880
|
+
currentChunk: completed,
|
|
881
|
+
totalChunks,
|
|
882
|
+
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
883
|
+
chunkIndex: index,
|
|
884
|
+
originalTextRange: input.originalTextRange,
|
|
885
|
+
status: "success",
|
|
886
|
+
durationMs: Date.now() - startedAt
|
|
887
|
+
});
|
|
888
|
+
} catch (error) {
|
|
889
|
+
failedIndices.add(index);
|
|
890
|
+
report({
|
|
891
|
+
currentChunk: completed,
|
|
892
|
+
totalChunks,
|
|
893
|
+
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
894
|
+
chunkIndex: index,
|
|
895
|
+
originalTextRange: input.originalTextRange,
|
|
896
|
+
status: "failed",
|
|
897
|
+
durationMs: Date.now() - startedAt,
|
|
898
|
+
error
|
|
899
|
+
});
|
|
900
|
+
firstError ?? (firstError = scope.timedOut() ? new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeouts?.totalJobMs} ms.`) : error);
|
|
901
|
+
if (config.cancelOnFailure !== false) scope.abort();
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
597
904
|
}
|
|
905
|
+
};
|
|
906
|
+
try {
|
|
907
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
908
|
+
if (firstError) throw firstError;
|
|
909
|
+
const orderedResults = results.filter((result) => result !== void 0);
|
|
910
|
+
return await mergeSynthesisResults(orderedResults, {
|
|
911
|
+
format: config.outputFormat ?? DEFAULT_OUTPUT_FORMAT,
|
|
912
|
+
signal: scope.signal,
|
|
913
|
+
customMerger: config.customMerger,
|
|
914
|
+
outputMimeType: config.outputMimeType,
|
|
915
|
+
postMergeValidator: config.postMergeValidator
|
|
916
|
+
});
|
|
917
|
+
} catch (error) {
|
|
918
|
+
const partial = {
|
|
919
|
+
synthesizedChunks: results.flatMap((result, chunkIndex) => result ? [{ ...result, chunkIndex }] : []),
|
|
920
|
+
completedChunks: results.flatMap((result, chunkIndex) => result ? [{ ...result, chunkIndex }] : []),
|
|
921
|
+
pendingChunkIndices: chunks.flatMap((_chunk, chunkIndex) => results[chunkIndex] ? [] : [chunkIndex]),
|
|
922
|
+
failedChunkIndices: [...failedIndices],
|
|
923
|
+
totalChunks
|
|
924
|
+
};
|
|
925
|
+
if (error && typeof error === "object") error.partialResult = partial;
|
|
926
|
+
throw error;
|
|
927
|
+
} finally {
|
|
928
|
+
scope.dispose();
|
|
598
929
|
}
|
|
599
|
-
return mergeSynthesisResults(results, {
|
|
600
|
-
format: config.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3"
|
|
601
|
-
});
|
|
602
930
|
}
|
|
603
|
-
function createMergedResult(results, audioData, format) {
|
|
931
|
+
function createMergedResult(results, audioData, format, audioSpec, outputMimeType) {
|
|
604
932
|
const boundaries = [];
|
|
605
933
|
const visemes = [];
|
|
606
934
|
const bookmarks = [];
|
|
@@ -619,7 +947,8 @@ function createMergedResult(results, audioData, format) {
|
|
|
619
947
|
...boundary.sourceNodePath ? { sourceNodePath: [...boundary.sourceNodePath] } : {},
|
|
620
948
|
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
621
949
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
622
|
-
...requestId ? { requestId } : {}
|
|
950
|
+
...requestId ? { requestId } : {},
|
|
951
|
+
mappingStatus: boundary.mappingStatus ?? "unmapped"
|
|
623
952
|
});
|
|
624
953
|
}
|
|
625
954
|
for (const viseme of result.visemes ?? []) {
|
|
@@ -634,7 +963,8 @@ function createMergedResult(results, audioData, format) {
|
|
|
634
963
|
...viseme.sourceNodePath ? { sourceNodePath: [...viseme.sourceNodePath] } : {},
|
|
635
964
|
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
636
965
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
637
|
-
...requestId ? { requestId } : {}
|
|
966
|
+
...requestId ? { requestId } : {},
|
|
967
|
+
mappingStatus: viseme.mappingStatus ?? "unmapped"
|
|
638
968
|
});
|
|
639
969
|
}
|
|
640
970
|
for (const bookmark of result.bookmarks ?? []) {
|
|
@@ -649,7 +979,8 @@ function createMergedResult(results, audioData, format) {
|
|
|
649
979
|
...bookmark.sourceNodePath ? { sourceNodePath: [...bookmark.sourceNodePath] } : {},
|
|
650
980
|
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
651
981
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
652
|
-
...requestId ? { requestId } : {}
|
|
982
|
+
...requestId ? { requestId } : {},
|
|
983
|
+
mappingStatus: bookmark.mappingStatus ?? "unmapped"
|
|
653
984
|
});
|
|
654
985
|
}
|
|
655
986
|
durationOffset += Math.max(0, result.durationMs);
|
|
@@ -658,6 +989,8 @@ function createMergedResult(results, audioData, format) {
|
|
|
658
989
|
audioData,
|
|
659
990
|
durationMs: durationOffset,
|
|
660
991
|
mimeType: resolveMimeType(format),
|
|
992
|
+
audioSpec: audioSpec ?? formatAudioSpecification(format),
|
|
993
|
+
...outputMimeType ? { mimeType: outputMimeType } : {},
|
|
661
994
|
...boundaries.length > 0 ? { boundaries, wordBoundary: boundaries, wordBoundaries: boundaries } : {},
|
|
662
995
|
...visemes.length > 0 ? { visemes } : {},
|
|
663
996
|
...bookmarks.length > 0 ? { bookmarks } : {},
|
|
@@ -670,19 +1003,73 @@ function mergeSynthesisResults(results, options) {
|
|
|
670
1003
|
const format = resolvedOptions?.format;
|
|
671
1004
|
if (!format) throw new UnsupportedMergeFormatError("");
|
|
672
1005
|
const buffers = results.map((result) => result.audioData);
|
|
1006
|
+
const inputSpecs = results.map((result) => result.audioSpec ?? inspectAudioSpecification(result.audioData, format));
|
|
1007
|
+
validateAudioSpecifications(inputSpecs);
|
|
1008
|
+
const signal = resolvedOptions.signal ?? new AbortController().signal;
|
|
1009
|
+
if (signal.aborted) throw new SynthesisCancelledError();
|
|
673
1010
|
if (resolvedOptions.customMerger) {
|
|
674
|
-
return Promise.resolve().then(
|
|
1011
|
+
return Promise.resolve().then(
|
|
1012
|
+
() => resolvedOptions.customMerger?.(buffers, {
|
|
1013
|
+
format,
|
|
1014
|
+
outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
1015
|
+
inputSpecs,
|
|
1016
|
+
signal
|
|
1017
|
+
})
|
|
1018
|
+
).then((merged) => {
|
|
675
1019
|
if (!merged) throw new MergeError("The custom audio merger returned no audio buffer.");
|
|
676
|
-
|
|
1020
|
+
if (!(merged instanceof ArrayBuffer) || buffers.some((buffer) => buffer.byteLength > 0) && merged.byteLength === 0)
|
|
1021
|
+
throw new MergeError("The custom audio merger returned an invalid audio buffer.");
|
|
1022
|
+
if (signal.aborted) throw new SynthesisCancelledError();
|
|
1023
|
+
const result = createMergedResult(
|
|
1024
|
+
results,
|
|
1025
|
+
merged,
|
|
1026
|
+
format,
|
|
1027
|
+
inspectAudioSpecification(merged, format),
|
|
1028
|
+
resolvedOptions.outputMimeType
|
|
1029
|
+
);
|
|
1030
|
+
return Promise.resolve(
|
|
1031
|
+
resolvedOptions.postMergeValidator?.(result, {
|
|
1032
|
+
format,
|
|
1033
|
+
outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
1034
|
+
inputSpecs,
|
|
1035
|
+
signal
|
|
1036
|
+
})
|
|
1037
|
+
).then((valid) => {
|
|
1038
|
+
if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1039
|
+
return result;
|
|
1040
|
+
});
|
|
677
1041
|
}).catch((error) => {
|
|
678
|
-
if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError)
|
|
1042
|
+
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
1043
|
+
throw error;
|
|
679
1044
|
throw new MergeError(`Custom audio merger failed for format "${format}".`, error);
|
|
680
1045
|
});
|
|
681
1046
|
}
|
|
682
1047
|
try {
|
|
683
|
-
|
|
1048
|
+
const result = createMergedResult(
|
|
1049
|
+
results,
|
|
1050
|
+
mergeAudioBuffers(buffers, { format }),
|
|
1051
|
+
format,
|
|
1052
|
+
inputSpecs[0],
|
|
1053
|
+
resolvedOptions.outputMimeType
|
|
1054
|
+
);
|
|
1055
|
+
if (resolvedOptions.postMergeValidator) {
|
|
1056
|
+
const validation = resolvedOptions.postMergeValidator(result, {
|
|
1057
|
+
format,
|
|
1058
|
+
outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
1059
|
+
inputSpecs,
|
|
1060
|
+
signal
|
|
1061
|
+
});
|
|
1062
|
+
if (validation instanceof Promise)
|
|
1063
|
+
return validation.then((valid) => {
|
|
1064
|
+
if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1065
|
+
return result;
|
|
1066
|
+
});
|
|
1067
|
+
if (validation === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1068
|
+
}
|
|
1069
|
+
return result;
|
|
684
1070
|
} catch (error) {
|
|
685
|
-
if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError)
|
|
1071
|
+
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
1072
|
+
throw error;
|
|
686
1073
|
throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
|
|
687
1074
|
}
|
|
688
1075
|
}
|
|
@@ -701,11 +1088,125 @@ var ChunkValidationError = class extends Error {
|
|
|
701
1088
|
this.diagnostics = diagnostics;
|
|
702
1089
|
}
|
|
703
1090
|
};
|
|
704
|
-
|
|
705
|
-
|
|
1091
|
+
var BatchChunkValidationError = class extends ChunkValidationError {
|
|
1092
|
+
constructor(chunkDiagnostics) {
|
|
1093
|
+
const first = chunkDiagnostics[0];
|
|
1094
|
+
super(first?.chunkIndex ?? -1, first?.diagnostics ?? []);
|
|
1095
|
+
this.name = "BatchChunkValidationError";
|
|
1096
|
+
this.message = `SSML validation failed for ${chunkDiagnostics.length} chunk(s); the Azure Speech API was not called.`;
|
|
1097
|
+
this.chunkDiagnostics = chunkDiagnostics;
|
|
1098
|
+
this.totalErrorCount = chunkDiagnostics.reduce((total, chunk) => total + chunk.diagnostics.length, 0);
|
|
1099
|
+
this.errorCount = this.totalErrorCount;
|
|
1100
|
+
this.totalErrors = this.totalErrorCount;
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
function failure(error, partialResult) {
|
|
1104
|
+
return {
|
|
1105
|
+
ok: false,
|
|
1106
|
+
success: false,
|
|
1107
|
+
status: error.kind,
|
|
1108
|
+
error,
|
|
1109
|
+
...partialResult ? { partialResult } : {}
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
function partialResultFrom(error) {
|
|
1113
|
+
if (!error || typeof error !== "object") return void 0;
|
|
1114
|
+
const partial = error.partialResult;
|
|
1115
|
+
if (!partial || typeof partial !== "object") return void 0;
|
|
1116
|
+
return partial;
|
|
1117
|
+
}
|
|
1118
|
+
function createSafeAbortScope(parent, timeoutMs) {
|
|
1119
|
+
const controller = new AbortController();
|
|
1120
|
+
let didTimeout = false;
|
|
1121
|
+
const onAbort = () => controller.abort();
|
|
1122
|
+
if (parent?.aborted) controller.abort();
|
|
1123
|
+
parent?.addEventListener("abort", onAbort, { once: true });
|
|
1124
|
+
const timer = timeoutMs !== void 0 && timeoutMs > 0 ? setTimeout(() => {
|
|
1125
|
+
didTimeout = true;
|
|
1126
|
+
controller.abort();
|
|
1127
|
+
}, timeoutMs) : void 0;
|
|
1128
|
+
return {
|
|
1129
|
+
signal: controller.signal,
|
|
1130
|
+
timedOut: () => didTimeout,
|
|
1131
|
+
dispose: () => {
|
|
1132
|
+
if (timer) clearTimeout(timer);
|
|
1133
|
+
parent?.removeEventListener("abort", onAbort);
|
|
1134
|
+
},
|
|
1135
|
+
abort: () => controller.abort()
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
function isRetryable(error) {
|
|
1139
|
+
if (error instanceof Error && /cancel|abort|tim(?:e|ed) ?out/i.test(error.message)) return false;
|
|
1140
|
+
const status = error && typeof error === "object" && "status" in error ? error.status : void 0;
|
|
1141
|
+
if (typeof status === "number" && status !== 0) return status === 429 || status >= 500 && status < 600;
|
|
1142
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1143
|
+
if (/\b4\d{2}\b/.test(message)) return false;
|
|
1144
|
+
return /network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message);
|
|
1145
|
+
}
|
|
1146
|
+
function delayForRetry(options, attempt) {
|
|
1147
|
+
const maxDelay = Math.max(0, options.maxDelayMs);
|
|
1148
|
+
const base = Math.min(maxDelay, Math.max(0, options.initialDelayMs) * 2 ** Math.max(0, attempt - 1));
|
|
1149
|
+
return Math.floor(Math.random() * (base + 1));
|
|
1150
|
+
}
|
|
1151
|
+
function retryDelayForError(options, attempt, error) {
|
|
1152
|
+
return getRetryAfterDelayMs(error) ?? delayForRetry(options, attempt);
|
|
1153
|
+
}
|
|
1154
|
+
function resolveConcurrency2(value, total) {
|
|
1155
|
+
if (value === void 0) return 1;
|
|
1156
|
+
if (value === Infinity) return Math.max(1, total);
|
|
1157
|
+
return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
|
|
1158
|
+
}
|
|
1159
|
+
async function retryableSynthesis(synthesize, options, signal, onRetry) {
|
|
1160
|
+
const retry = options ? {
|
|
1161
|
+
maxRetries: Math.max(0, Math.floor(options.maxRetries)),
|
|
1162
|
+
initialDelayMs: options.initialDelayMs,
|
|
1163
|
+
maxDelayMs: options.maxDelayMs,
|
|
1164
|
+
shouldRetry: options.shouldRetry
|
|
1165
|
+
} : void 0;
|
|
1166
|
+
let attempt = 0;
|
|
1167
|
+
while (true) {
|
|
1168
|
+
if (signal?.aborted) throw new Error("Speech synthesis was cancelled.");
|
|
1169
|
+
try {
|
|
1170
|
+
return await synthesize();
|
|
1171
|
+
} catch (error) {
|
|
1172
|
+
if (!retry || attempt >= retry.maxRetries || !(retry.shouldRetry?.(error, attempt + 1) ?? isRetryable(error)))
|
|
1173
|
+
throw error;
|
|
1174
|
+
attempt += 1;
|
|
1175
|
+
const delayMs = retryDelayForError(retry, attempt, error);
|
|
1176
|
+
onRetry(attempt, delayMs);
|
|
1177
|
+
if (delayMs > 0)
|
|
1178
|
+
await new Promise((resolve, reject) => {
|
|
1179
|
+
const timer = setTimeout(() => {
|
|
1180
|
+
signal?.removeEventListener("abort", abort);
|
|
1181
|
+
resolve();
|
|
1182
|
+
}, delayMs);
|
|
1183
|
+
const abort = () => {
|
|
1184
|
+
clearTimeout(timer);
|
|
1185
|
+
signal?.removeEventListener("abort", abort);
|
|
1186
|
+
reject(new Error("Speech synthesis was cancelled."));
|
|
1187
|
+
};
|
|
1188
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
function sharedValidationOptions(options, signal) {
|
|
1194
|
+
const validator = options.urlValidator ?? options.customUrlValidator;
|
|
1195
|
+
if (!validator) return signal ? withValidationSignal(options, signal) : options;
|
|
1196
|
+
const runner = (0, import_ssml_core2.createAzureUrlValidatorRunner)(validator, {
|
|
1197
|
+
...options.urlValidation ?? {},
|
|
1198
|
+
...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
|
|
1199
|
+
...options.timeouts?.urlValidationMs !== void 0 ? { timeoutMs: options.timeouts.urlValidationMs } : options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
|
|
1200
|
+
...signal ? { signal } : {},
|
|
1201
|
+
...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
|
|
1202
|
+
});
|
|
1203
|
+
return {
|
|
1204
|
+
...withValidationSignal(options, signal),
|
|
1205
|
+
urlValidatorRunner: runner
|
|
1206
|
+
};
|
|
706
1207
|
}
|
|
707
1208
|
async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
708
|
-
const validationOptions =
|
|
1209
|
+
const validationOptions = sharedValidationOptions(options.validation ?? options, options.signal);
|
|
709
1210
|
const diagnostics = await Promise.resolve((0, import_ssml_core2.validateAzureSsml)(ssml, validationOptions));
|
|
710
1211
|
if (options.signal?.aborted) {
|
|
711
1212
|
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
@@ -724,7 +1225,11 @@ async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
|
724
1225
|
ok: true,
|
|
725
1226
|
success: true,
|
|
726
1227
|
status: "success",
|
|
727
|
-
value: await client.synthesizeSsml(ssml, {
|
|
1228
|
+
value: await client.synthesizeSsml(ssml, {
|
|
1229
|
+
signal: options.signal,
|
|
1230
|
+
timeoutMs: options.timeouts?.perChunkMs,
|
|
1231
|
+
timeouts: options.timeouts
|
|
1232
|
+
})
|
|
728
1233
|
};
|
|
729
1234
|
} catch (error) {
|
|
730
1235
|
const synthesisError = toSynthesisError(error);
|
|
@@ -732,7 +1237,10 @@ async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
|
732
1237
|
}
|
|
733
1238
|
}
|
|
734
1239
|
async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
735
|
-
const validationOptions =
|
|
1240
|
+
const validationOptions = sharedValidationOptions(
|
|
1241
|
+
{ ...options.validation ?? options, timeouts: options.timeouts },
|
|
1242
|
+
options.signal
|
|
1243
|
+
);
|
|
736
1244
|
if (options.signal?.aborted) {
|
|
737
1245
|
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
738
1246
|
return failure(error);
|
|
@@ -753,21 +1261,30 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
753
1261
|
pending(index, "pending");
|
|
754
1262
|
});
|
|
755
1263
|
const validations = await Promise.all(
|
|
756
|
-
chunks.map(async (chunk) => {
|
|
1264
|
+
chunks.map(async (chunk, index) => {
|
|
757
1265
|
const ssml = typeof chunk === "string" ? chunk : chunk.ssml;
|
|
758
1266
|
const sourceNodePath = typeof chunk === "string" ? options.sourceNodePath : chunk.sourceNodePath ?? options.sourceNodePath;
|
|
759
1267
|
const diagnostics = await Promise.resolve(
|
|
760
|
-
(0, import_ssml_core2.validateAzureSsml)(ssml, {
|
|
1268
|
+
(0, import_ssml_core2.validateAzureSsml)(ssml, {
|
|
1269
|
+
...validationOptions,
|
|
1270
|
+
...sourceNodePath ? { sourceNodePath } : {},
|
|
1271
|
+
chunkIndex: index
|
|
1272
|
+
})
|
|
761
1273
|
);
|
|
762
1274
|
return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
763
1275
|
})
|
|
764
1276
|
);
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
const error = new ChunkValidationError(firstInvalidIndex, validations[firstInvalidIndex] ?? []);
|
|
768
|
-
pending(firstInvalidIndex, "failed", error);
|
|
1277
|
+
if (options.signal?.aborted) {
|
|
1278
|
+
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
769
1279
|
return failure(error);
|
|
770
1280
|
}
|
|
1281
|
+
const chunkDiagnostics = validations.map((diagnostics, chunkIndex) => ({ chunkIndex, diagnostics })).filter((entry) => entry.diagnostics.length > 0);
|
|
1282
|
+
if (chunkDiagnostics.length > 0) {
|
|
1283
|
+
const error = new BatchChunkValidationError(chunkDiagnostics);
|
|
1284
|
+
for (const entry of chunkDiagnostics) pending(entry.chunkIndex, "failed", error);
|
|
1285
|
+
return failure(error);
|
|
1286
|
+
}
|
|
1287
|
+
let fallbackJobScope;
|
|
771
1288
|
try {
|
|
772
1289
|
if (client.synthesizeChunks) {
|
|
773
1290
|
const normalizedChunks = chunks.map((chunk) => {
|
|
@@ -779,101 +1296,181 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
779
1296
|
outputFormat: options.outputFormat,
|
|
780
1297
|
signal: options.signal,
|
|
781
1298
|
timeoutMs: options.timeoutMs,
|
|
782
|
-
|
|
1299
|
+
timeouts: options.timeouts,
|
|
1300
|
+
sourceNodePath: options.sourceNodePath,
|
|
1301
|
+
concurrency: options.concurrency,
|
|
1302
|
+
retryOptions: options.retryOptions,
|
|
1303
|
+
cancelOnFailure: options.cancelOnFailure,
|
|
1304
|
+
resumeChunks: options.resumeChunks,
|
|
1305
|
+
resumeChunkIndices: options.resumeChunkIndices,
|
|
1306
|
+
customMerger: options.customMerger,
|
|
1307
|
+
outputMimeType: options.outputMimeType,
|
|
1308
|
+
postMergeValidator: options.postMergeValidator
|
|
783
1309
|
});
|
|
784
1310
|
return { ok: true, success: true, status: "success", value };
|
|
785
1311
|
}
|
|
786
|
-
const results =
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
1312
|
+
const results = new Array(chunks.length);
|
|
1313
|
+
const cachedChunks = new Map((options.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
|
|
1314
|
+
for (const [index, cached] of cachedChunks) {
|
|
1315
|
+
if (index >= 0 && index < chunks.length) results[index] = cached;
|
|
1316
|
+
}
|
|
1317
|
+
const requestedIndices = options.resumeChunkIndices ? new Set(options.resumeChunkIndices.filter((index) => index >= 0 && index < chunks.length)) : void 0;
|
|
1318
|
+
const shouldSynthesize = (index) => !cachedChunks.has(index) && (requestedIndices === void 0 || requestedIndices.has(index));
|
|
1319
|
+
const jobScope = chunks.length > 1 || options.timeouts?.totalJobMs !== void 0 ? createSafeAbortScope(options.signal, options.timeouts?.totalJobMs) : void 0;
|
|
1320
|
+
fallbackJobScope = jobScope;
|
|
1321
|
+
const failedIndices = /* @__PURE__ */ new Set();
|
|
1322
|
+
let firstError;
|
|
1323
|
+
let completed = [...results].filter((result) => result !== void 0).length;
|
|
1324
|
+
let nextIndex = 0;
|
|
1325
|
+
const concurrency = resolveConcurrency2(options.concurrency, chunks.length);
|
|
1326
|
+
const worker = async () => {
|
|
1327
|
+
while (true) {
|
|
1328
|
+
const index = nextIndex++;
|
|
1329
|
+
if (index >= chunks.length) return;
|
|
1330
|
+
if (!shouldSynthesize(index)) continue;
|
|
1331
|
+
if (failedIndices.size > 0 && options.cancelOnFailure !== false) return;
|
|
1332
|
+
const chunk = chunks[index];
|
|
1333
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
1334
|
+
const sourceNodePath = input.sourceNodePath;
|
|
1335
|
+
const originalTextRange = input.originalTextRange;
|
|
1336
|
+
pending(index, "synthesizing");
|
|
1337
|
+
const startedAt = Date.now();
|
|
1338
|
+
try {
|
|
1339
|
+
const chunkTimeout = options.timeouts?.chunkWithRetriesMs ?? options.timeouts?.perChunkMs;
|
|
1340
|
+
const chunkScope = chunkTimeout !== void 0 || jobScope ? createSafeAbortScope(jobScope?.signal ?? options.signal, chunkTimeout ?? options.timeoutMs) : void 0;
|
|
1341
|
+
const chunkSignal = chunkScope?.signal ?? options.signal;
|
|
1342
|
+
let result;
|
|
1343
|
+
try {
|
|
1344
|
+
result = await retryableSynthesis(
|
|
1345
|
+
() => client.synthesizeSsml(input.ssml, {
|
|
1346
|
+
outputFormat: options.outputFormat,
|
|
1347
|
+
signal: chunkSignal,
|
|
1348
|
+
timeoutMs: options.timeouts?.perChunkMs ?? options.timeoutMs,
|
|
1349
|
+
sourceNodePath: input.sourceNodePath ?? options.sourceNodePath
|
|
1350
|
+
}),
|
|
1351
|
+
options.retryOptions,
|
|
1352
|
+
chunkSignal,
|
|
1353
|
+
(retryAttempt, nextRetryDelayMs) => options.onProgress?.({
|
|
1354
|
+
currentChunk: completed,
|
|
1355
|
+
totalChunks: chunks.length,
|
|
1356
|
+
percent: chunks.length === 0 ? 100 : Math.round(completed / chunks.length * 100),
|
|
1357
|
+
chunkIndex: index,
|
|
1358
|
+
originalTextRange: input.originalTextRange,
|
|
1359
|
+
status: "synthesizing",
|
|
1360
|
+
durationMs: Date.now() - startedAt,
|
|
1361
|
+
retryAttempt,
|
|
1362
|
+
nextRetryDelayMs,
|
|
1363
|
+
isRetrying: true
|
|
1364
|
+
})
|
|
1365
|
+
);
|
|
1366
|
+
} catch (error) {
|
|
1367
|
+
if (chunkScope?.timedOut())
|
|
1368
|
+
throw new Error(`Speech synthesis timed out after ${chunkTimeout ?? options.timeoutMs} ms.`);
|
|
1369
|
+
throw error;
|
|
1370
|
+
} finally {
|
|
1371
|
+
chunkScope?.dispose();
|
|
1372
|
+
}
|
|
1373
|
+
results[index] = {
|
|
1374
|
+
...result,
|
|
1375
|
+
...input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {},
|
|
1376
|
+
...sourceNodePath ? {
|
|
1377
|
+
boundaries: result.boundaries?.map((event) => ({
|
|
1378
|
+
...event,
|
|
1379
|
+
sourceNodePath: [...sourceNodePath],
|
|
1380
|
+
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
1381
|
+
})),
|
|
1382
|
+
visemes: result.visemes?.map((event) => ({
|
|
1383
|
+
...event,
|
|
1384
|
+
sourceNodePath: [...sourceNodePath],
|
|
1385
|
+
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
1386
|
+
})),
|
|
1387
|
+
bookmarks: result.bookmarks?.map((event) => ({
|
|
1388
|
+
...event,
|
|
1389
|
+
sourceNodePath: [...sourceNodePath],
|
|
1390
|
+
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
1391
|
+
}))
|
|
1392
|
+
} : {},
|
|
1393
|
+
...originalTextRange ? {
|
|
1394
|
+
boundaries: result.boundaries?.map((event) => ({
|
|
1395
|
+
...event,
|
|
1396
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1397
|
+
})),
|
|
1398
|
+
wordBoundary: result.wordBoundary?.map((event) => ({
|
|
1399
|
+
...event,
|
|
1400
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1401
|
+
})),
|
|
1402
|
+
wordBoundaries: result.wordBoundaries?.map((event) => ({
|
|
1403
|
+
...event,
|
|
1404
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1405
|
+
})),
|
|
1406
|
+
visemes: result.visemes?.map((event) => ({
|
|
1407
|
+
...event,
|
|
1408
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1409
|
+
})),
|
|
1410
|
+
bookmarks: result.bookmarks?.map((event) => ({
|
|
1411
|
+
...event,
|
|
1412
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1413
|
+
}))
|
|
1414
|
+
} : {}
|
|
1415
|
+
};
|
|
1416
|
+
completed += 1;
|
|
1417
|
+
options.onProgress?.({
|
|
1418
|
+
currentChunk: completed,
|
|
1419
|
+
totalChunks: chunks.length,
|
|
1420
|
+
percent: chunks.length === 0 ? 100 : Math.round(completed / chunks.length * 100),
|
|
1421
|
+
chunkIndex: index,
|
|
1422
|
+
originalTextRange: input.originalTextRange,
|
|
1423
|
+
status: "success",
|
|
1424
|
+
durationMs: Date.now() - startedAt
|
|
1425
|
+
});
|
|
1426
|
+
} catch (error) {
|
|
1427
|
+
failedIndices.add(index);
|
|
1428
|
+
options.onProgress?.({
|
|
1429
|
+
currentChunk: completed,
|
|
1430
|
+
totalChunks: chunks.length,
|
|
1431
|
+
percent: chunks.length === 0 ? 100 : Math.round(index / chunks.length * 100),
|
|
1432
|
+
chunkIndex: index,
|
|
1433
|
+
originalTextRange: input.originalTextRange,
|
|
1434
|
+
status: "failed",
|
|
1435
|
+
durationMs: Date.now() - startedAt,
|
|
1436
|
+
error
|
|
1437
|
+
});
|
|
1438
|
+
if (options.cancelOnFailure !== false) jobScope?.abort();
|
|
1439
|
+
firstError ?? (firstError = error);
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
864
1442
|
}
|
|
1443
|
+
};
|
|
1444
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
1445
|
+
if (failedIndices.size > 0) {
|
|
1446
|
+
const error = firstError ?? new Error("One or more SSML chunks failed to synthesize.");
|
|
1447
|
+
error.partialResult = {
|
|
1448
|
+
synthesizedChunks: results.flatMap((result, chunkIndex) => result ? [{ ...result, chunkIndex }] : []),
|
|
1449
|
+
completedChunks: results.flatMap((result, chunkIndex) => result ? [{ ...result, chunkIndex }] : []),
|
|
1450
|
+
pendingChunkIndices: chunks.flatMap((_chunk, chunkIndex) => results[chunkIndex] ? [] : [chunkIndex]),
|
|
1451
|
+
failedChunkIndices: [...failedIndices],
|
|
1452
|
+
totalChunks: chunks.length
|
|
1453
|
+
};
|
|
1454
|
+
throw error;
|
|
865
1455
|
}
|
|
1456
|
+
const orderedResults = results.filter((result) => result !== void 0);
|
|
866
1457
|
return {
|
|
867
1458
|
ok: true,
|
|
868
1459
|
success: true,
|
|
869
1460
|
status: "success",
|
|
870
|
-
value: mergeSynthesisResults(
|
|
871
|
-
format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3"
|
|
1461
|
+
value: await mergeSynthesisResults(orderedResults, {
|
|
1462
|
+
format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3",
|
|
1463
|
+
signal: jobScope?.signal ?? options.signal,
|
|
1464
|
+
customMerger: options.customMerger,
|
|
1465
|
+
outputMimeType: options.outputMimeType,
|
|
1466
|
+
postMergeValidator: options.postMergeValidator
|
|
872
1467
|
})
|
|
873
1468
|
};
|
|
874
1469
|
} catch (error) {
|
|
875
1470
|
const synthesisError = toSynthesisError(error);
|
|
876
|
-
return failure(synthesisError);
|
|
1471
|
+
return failure(synthesisError, partialResultFrom(error));
|
|
1472
|
+
} finally {
|
|
1473
|
+
fallbackJobScope?.dispose();
|
|
877
1474
|
}
|
|
878
1475
|
}
|
|
879
1476
|
function withValidationSignal(options, signal) {
|
|
@@ -894,14 +1491,14 @@ var AzureTtsClient = class {
|
|
|
894
1491
|
__privateSet(this, _options, options);
|
|
895
1492
|
}
|
|
896
1493
|
async synthesize(ssml) {
|
|
897
|
-
const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
|
|
1494
|
+
const { region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts } = __privateGet(this, _options);
|
|
898
1495
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
899
1496
|
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
900
|
-
const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs };
|
|
1497
|
+
const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts };
|
|
901
1498
|
return synthesizeSpeech(ssml, config);
|
|
902
1499
|
}
|
|
903
1500
|
async synthesizeSsml(ssml, options = {}) {
|
|
904
|
-
const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
|
|
1501
|
+
const { region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts } = __privateGet(this, _options);
|
|
905
1502
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
906
1503
|
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
907
1504
|
return synthesizeSsml(ssml, {
|
|
@@ -911,13 +1508,14 @@ var AzureTtsClient = class {
|
|
|
911
1508
|
outputFormat: options.outputFormat ?? outputFormat,
|
|
912
1509
|
signal: options.signal ?? signal,
|
|
913
1510
|
timeoutMs: options.timeoutMs ?? timeoutMs,
|
|
1511
|
+
timeouts: options.timeouts ?? timeouts,
|
|
914
1512
|
sourceNodePath: options.sourceNodePath,
|
|
915
1513
|
sourceTextSegments: options.sourceTextSegments,
|
|
916
1514
|
sourceMarkers: options.sourceMarkers
|
|
917
1515
|
});
|
|
918
1516
|
}
|
|
919
1517
|
async synthesizeChunks(chunks, options = {}) {
|
|
920
|
-
const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
|
|
1518
|
+
const { region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts } = __privateGet(this, _options);
|
|
921
1519
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
922
1520
|
return synthesizeSsmlChunks(chunks, {
|
|
923
1521
|
endpoint,
|
|
@@ -926,8 +1524,17 @@ var AzureTtsClient = class {
|
|
|
926
1524
|
outputFormat: options.outputFormat ?? outputFormat,
|
|
927
1525
|
signal: options.signal ?? signal,
|
|
928
1526
|
timeoutMs: options.timeoutMs ?? timeoutMs,
|
|
1527
|
+
timeouts: options.timeouts ?? timeouts,
|
|
929
1528
|
sourceNodePath: options.sourceNodePath,
|
|
930
|
-
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
|
|
1529
|
+
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
1530
|
+
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
1531
|
+
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
1532
|
+
cancelOnFailure: options.cancelOnFailure,
|
|
1533
|
+
resumeChunks: options.resumeChunks,
|
|
1534
|
+
resumeChunkIndices: options.resumeChunkIndices,
|
|
1535
|
+
customMerger: options.customMerger,
|
|
1536
|
+
outputMimeType: options.outputMimeType,
|
|
1537
|
+
postMergeValidator: options.postMergeValidator
|
|
931
1538
|
});
|
|
932
1539
|
}
|
|
933
1540
|
async synthesizeSsmlSafe(ssml, options = {}) {
|
|
@@ -939,7 +1546,10 @@ var AzureTtsClient = class {
|
|
|
939
1546
|
outputFormat: options.outputFormat ?? __privateGet(this, _options).outputFormat,
|
|
940
1547
|
signal: options.signal ?? __privateGet(this, _options).signal,
|
|
941
1548
|
timeoutMs: options.timeoutMs ?? __privateGet(this, _options).timeoutMs,
|
|
942
|
-
|
|
1549
|
+
timeouts: options.timeouts ?? __privateGet(this, _options).timeouts,
|
|
1550
|
+
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
1551
|
+
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
1552
|
+
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions
|
|
943
1553
|
});
|
|
944
1554
|
}
|
|
945
1555
|
async synthesizeSsmlChunksSafe(chunks, options = {}) {
|
|
@@ -1030,15 +1640,19 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
1030
1640
|
voiceCount: sortedVoices.length,
|
|
1031
1641
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1032
1642
|
apiVersion: AZURE_VOICE_API_VERSION,
|
|
1033
|
-
regions
|
|
1643
|
+
regions,
|
|
1644
|
+
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3).toISOString(),
|
|
1645
|
+
regionDiffs: {}
|
|
1034
1646
|
}
|
|
1035
1647
|
};
|
|
1036
1648
|
}
|
|
1037
1649
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1038
1650
|
0 && (module.exports = {
|
|
1651
|
+
AudioFormatMismatchError,
|
|
1039
1652
|
AzureTtsClient,
|
|
1040
1653
|
AzureTtsError,
|
|
1041
1654
|
AzureTtsSdkError,
|
|
1655
|
+
BatchChunkValidationError,
|
|
1042
1656
|
ChunkValidationError,
|
|
1043
1657
|
DEFAULT_OUTPUT_FORMAT,
|
|
1044
1658
|
MergeError,
|
|
@@ -1047,6 +1661,8 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
1047
1661
|
UnsupportedMergeFormatError,
|
|
1048
1662
|
canMergeAudioFormat,
|
|
1049
1663
|
fetchAzureVoiceCatalog,
|
|
1664
|
+
getRetryAfterDelayMs,
|
|
1665
|
+
inspectAudioSpecification,
|
|
1050
1666
|
mergeAudioBuffers,
|
|
1051
1667
|
mergeSynthesisResults,
|
|
1052
1668
|
resolveMergeAudioFormat,
|