@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.mjs
CHANGED
|
@@ -8,7 +8,7 @@ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "
|
|
|
8
8
|
|
|
9
9
|
// src/errors.ts
|
|
10
10
|
var AzureTtsError = class extends Error {
|
|
11
|
-
constructor(status, statusText, responseBody, requestId) {
|
|
11
|
+
constructor(status, statusText, responseBody, requestId, responseHeaders) {
|
|
12
12
|
super(`Azure TTS request failed: ${status} ${statusText}`);
|
|
13
13
|
this.kind = "azure-api-error";
|
|
14
14
|
this.name = "AzureTtsError";
|
|
@@ -16,8 +16,37 @@ var AzureTtsError = class extends Error {
|
|
|
16
16
|
this.statusText = statusText;
|
|
17
17
|
this.responseBody = responseBody;
|
|
18
18
|
this.requestId = requestId;
|
|
19
|
+
const value = responseHeaders instanceof Headers ? responseHeaders.get("retry-after") : responseHeaders?.["retry-after"] ?? responseHeaders?.["Retry-After"];
|
|
20
|
+
const seconds = value ? Number(value.trim()) : NaN;
|
|
21
|
+
const date = value ? Date.parse(value) : NaN;
|
|
22
|
+
if (Number.isFinite(seconds) && seconds >= 0) this.retryAfterMs = seconds * 1e3;
|
|
23
|
+
else if (Number.isFinite(date)) this.retryAfterMs = Math.max(0, date - Date.now());
|
|
19
24
|
}
|
|
20
25
|
};
|
|
26
|
+
function getRetryAfterDelayMs(error) {
|
|
27
|
+
if (error instanceof AzureTtsError && error.retryAfterMs !== void 0) return error.retryAfterMs;
|
|
28
|
+
if (!error || typeof error !== "object") return void 0;
|
|
29
|
+
const candidate = error;
|
|
30
|
+
if (typeof candidate.retryAfterMs === "number" && candidate.retryAfterMs >= 0) return candidate.retryAfterMs;
|
|
31
|
+
const headers = candidate.headers ?? candidate.response?.headers;
|
|
32
|
+
if (headers instanceof Headers) {
|
|
33
|
+
const value = headers.get("retry-after");
|
|
34
|
+
if (!value) return void 0;
|
|
35
|
+
const seconds = Number(value.trim());
|
|
36
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
|
|
37
|
+
const date = Date.parse(value);
|
|
38
|
+
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : void 0;
|
|
39
|
+
}
|
|
40
|
+
if (headers && typeof headers === "object") {
|
|
41
|
+
const value = headers["retry-after"] ?? headers["Retry-After"];
|
|
42
|
+
if (typeof value !== "string") return void 0;
|
|
43
|
+
const seconds = Number(value.trim());
|
|
44
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
|
|
45
|
+
const date = Date.parse(value);
|
|
46
|
+
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : void 0;
|
|
47
|
+
}
|
|
48
|
+
return void 0;
|
|
49
|
+
}
|
|
21
50
|
var AzureTtsSdkError = class extends AzureTtsError {
|
|
22
51
|
constructor(errorDetails) {
|
|
23
52
|
super(0, "Speech SDK", errorDetails, null);
|
|
@@ -48,6 +77,14 @@ var MergeError = class extends Error {
|
|
|
48
77
|
this.cause = cause;
|
|
49
78
|
}
|
|
50
79
|
};
|
|
80
|
+
var AudioFormatMismatchError = class extends Error {
|
|
81
|
+
constructor(message, inputSpecs = []) {
|
|
82
|
+
super(message);
|
|
83
|
+
this.kind = "audio-format-mismatch";
|
|
84
|
+
this.name = "AudioFormatMismatchError";
|
|
85
|
+
this.inputSpecs = inputSpecs;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
51
88
|
var UnsupportedMergeFormatError = class extends Error {
|
|
52
89
|
constructor(format) {
|
|
53
90
|
super(`Audio format "${format}" cannot be safely concatenated; container re-multiplexing is required.`);
|
|
@@ -57,7 +94,7 @@ var UnsupportedMergeFormatError = class extends Error {
|
|
|
57
94
|
}
|
|
58
95
|
};
|
|
59
96
|
function toSynthesisError(error) {
|
|
60
|
-
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
|
|
97
|
+
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof AudioFormatMismatchError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
|
|
61
98
|
return error;
|
|
62
99
|
const message = error instanceof Error ? error.message : String(error);
|
|
63
100
|
if (/cancel|abort/i.test(message)) return new SynthesisCancelledError(message);
|
|
@@ -187,6 +224,115 @@ function parseWav(buffer) {
|
|
|
187
224
|
}
|
|
188
225
|
return { chunks, data, format };
|
|
189
226
|
}
|
|
227
|
+
function formatNumber(format, pattern, fallback) {
|
|
228
|
+
const match = pattern.exec(format);
|
|
229
|
+
return match?.[1] ? Number(match[1]) : fallback;
|
|
230
|
+
}
|
|
231
|
+
function formatChannels(format, fallback) {
|
|
232
|
+
if (/stereo|2ch|dual/i.test(format)) return 2;
|
|
233
|
+
if (/mono|1ch/i.test(format)) return 1;
|
|
234
|
+
return fallback;
|
|
235
|
+
}
|
|
236
|
+
function formatAudioSpecification(format) {
|
|
237
|
+
const sampleRate = formatNumber(format, /(\d+)(?:khz|kHz|hz|Hz)/, 0);
|
|
238
|
+
const channels = formatChannels(format, 0);
|
|
239
|
+
const bitrateMatch = /(\d+)(?:kbitrate|kbps|kbits?)/i.exec(format);
|
|
240
|
+
const bitrate = bitrateMatch?.[1] ? Number(bitrateMatch[1]) * 1e3 : void 0;
|
|
241
|
+
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";
|
|
242
|
+
const bitDepthMatch = /(\d+)bit/i.exec(format);
|
|
243
|
+
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;
|
|
244
|
+
return {
|
|
245
|
+
format,
|
|
246
|
+
mimeType: resolveMimeType(format),
|
|
247
|
+
codec,
|
|
248
|
+
sampleRate,
|
|
249
|
+
channels,
|
|
250
|
+
...bitrate ? { bitrate } : {},
|
|
251
|
+
...bitDepthMatch?.[1] ? { bitDepth: Number(bitDepthMatch[1]) } : {},
|
|
252
|
+
...container ? { container } : {},
|
|
253
|
+
isVbr: /vbr/i.test(format),
|
|
254
|
+
isCompressed: codec === "mp3" || codec === "opus" || codec === "silk"
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function parseMp3Specification(buffer, format) {
|
|
258
|
+
const bytes = stripMp3Tags(buffer);
|
|
259
|
+
const bitrates = [
|
|
260
|
+
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
261
|
+
[0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0],
|
|
262
|
+
[0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0]
|
|
263
|
+
];
|
|
264
|
+
const sampleRates = [
|
|
265
|
+
[44100, 48e3, 32e3],
|
|
266
|
+
[22050, 24e3, 16e3],
|
|
267
|
+
[11025, 12e3, 8e3]
|
|
268
|
+
];
|
|
269
|
+
for (let index = 0; index + 4 <= bytes.length; index += 1) {
|
|
270
|
+
if (bytes[index] !== 255 || (bytes[index + 1] ?? 0) < 224) continue;
|
|
271
|
+
const header = bytes[index + 1] ?? 0;
|
|
272
|
+
const versionBits = header >> 3 & 3;
|
|
273
|
+
const layer = header >> 1 & 3;
|
|
274
|
+
const bitrateIndex = (bytes[index + 2] ?? 0) >> 4;
|
|
275
|
+
const sampleIndex = (bytes[index + 2] ?? 0) >> 2 & 3;
|
|
276
|
+
if (versionBits === 1 || layer !== 1 || bitrateIndex === 0 || bitrateIndex === 15 || sampleIndex === 3) continue;
|
|
277
|
+
const versionIndex = versionBits === 3 ? 0 : versionBits === 2 ? 1 : 2;
|
|
278
|
+
const bitrateTable = versionBits === 3 ? bitrates[1] : bitrates[2];
|
|
279
|
+
const sampleRate = sampleRates[versionIndex]?.[sampleIndex] ?? 0;
|
|
280
|
+
const bitrateKbps = bitrateTable?.[bitrateIndex] ?? 0;
|
|
281
|
+
if (!sampleRate || !bitrateKbps) continue;
|
|
282
|
+
return {
|
|
283
|
+
format,
|
|
284
|
+
mimeType: "audio/mpeg",
|
|
285
|
+
codec: "mp3",
|
|
286
|
+
sampleRate,
|
|
287
|
+
channels: (bytes[index + 3] ?? 0) >> 6 === 3 ? 1 : 2,
|
|
288
|
+
bitrate: bitrateKbps * 1e3,
|
|
289
|
+
container: "mp3-raw",
|
|
290
|
+
isVbr: false,
|
|
291
|
+
isCompressed: true
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
return void 0;
|
|
295
|
+
}
|
|
296
|
+
function inspectAudioSpecification(buffer, format) {
|
|
297
|
+
if (isWavFormat(format) || ascii(new Uint8Array(buffer), 0, "RIFF")) {
|
|
298
|
+
const parsed = parseWav(buffer);
|
|
299
|
+
if (parsed.format.byteLength < 16) throw new Error("Invalid WAV fmt chunk.");
|
|
300
|
+
const view = new DataView(parsed.format.buffer, parsed.format.byteOffset, parsed.format.byteLength);
|
|
301
|
+
const sampleRate = view.getUint32(4, true);
|
|
302
|
+
const channels = view.getUint16(2, true);
|
|
303
|
+
const bitsPerSample = parsed.format.byteLength >= 16 ? view.getUint16(14, true) : 0;
|
|
304
|
+
const formatCode = view.getUint16(0, true);
|
|
305
|
+
return {
|
|
306
|
+
format,
|
|
307
|
+
mimeType: "audio/wav",
|
|
308
|
+
codec: formatCode === 1 ? "pcm" : "unknown",
|
|
309
|
+
sampleRate,
|
|
310
|
+
channels,
|
|
311
|
+
...sampleRate && channels && bitsPerSample ? { bitrate: sampleRate * channels * bitsPerSample } : {},
|
|
312
|
+
bitDepth: bitsPerSample,
|
|
313
|
+
container: "riff-wave",
|
|
314
|
+
isVbr: false,
|
|
315
|
+
isCompressed: formatCode !== 1
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
if (isMp3Format(format)) return parseMp3Specification(buffer, format) ?? formatAudioSpecification(format);
|
|
319
|
+
return formatAudioSpecification(format);
|
|
320
|
+
}
|
|
321
|
+
function validateAudioSpecifications(specs) {
|
|
322
|
+
const first = specs[0];
|
|
323
|
+
if (!first) return;
|
|
324
|
+
const mismatch = specs.find(
|
|
325
|
+
(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
|
|
326
|
+
);
|
|
327
|
+
if (mismatch)
|
|
328
|
+
throw new AudioFormatMismatchError(
|
|
329
|
+
`Audio chunks have incompatible specifications: ${first.sampleRate}Hz/${first.channels}ch versus ${mismatch.sampleRate}Hz/${mismatch.channels}ch.`,
|
|
330
|
+
specs
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
function isAudioFormatMismatch(error) {
|
|
334
|
+
return error instanceof AudioFormatMismatchError || error !== null && typeof error === "object" && "kind" in error && error.kind === "audio-format-mismatch";
|
|
335
|
+
}
|
|
190
336
|
function writeUint32(target, offset, value) {
|
|
191
337
|
new DataView(target.buffer).setUint32(offset, value, true);
|
|
192
338
|
}
|
|
@@ -272,6 +418,7 @@ function mergeAudioBuffers(buffers, options) {
|
|
|
272
418
|
const format = typeof options === "string" ? options : options?.format;
|
|
273
419
|
if (!format) throw new UnsupportedMergeFormatError("");
|
|
274
420
|
try {
|
|
421
|
+
validateAudioSpecifications(buffers.map((buffer) => inspectAudioSpecification(buffer, format)));
|
|
275
422
|
if (isWavFormat(format)) return mergeWavBuffers(buffers);
|
|
276
423
|
if (isMp3Format(format)) {
|
|
277
424
|
const parts = buffers.map(stripMp3Tags);
|
|
@@ -294,7 +441,8 @@ function mergeAudioBuffers(buffers, options) {
|
|
|
294
441
|
}
|
|
295
442
|
throw new UnsupportedMergeFormatError(format);
|
|
296
443
|
} catch (error) {
|
|
297
|
-
if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError)
|
|
444
|
+
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
445
|
+
throw error;
|
|
298
446
|
throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
|
|
299
447
|
}
|
|
300
448
|
}
|
|
@@ -372,17 +520,24 @@ async function synthesizeSsml(ssml, config) {
|
|
|
372
520
|
return {
|
|
373
521
|
originalTextRange: { ...marker.originalTextRange },
|
|
374
522
|
sourceNodePath: [...marker.sourceNodePath],
|
|
375
|
-
textRange: { ...marker.originalTextRange }
|
|
523
|
+
textRange: { ...marker.originalTextRange },
|
|
524
|
+
mappingStatus: "exact"
|
|
376
525
|
};
|
|
377
526
|
}
|
|
378
|
-
if (sourceSegments.length === 0 && !config.sourceTextRange && !config.sourceNodePath)
|
|
527
|
+
if (sourceSegments.length === 0 && !config.sourceTextRange && !config.sourceNodePath) {
|
|
528
|
+
return { mappingStatus: "unmapped" };
|
|
529
|
+
}
|
|
379
530
|
const value = text ?? "";
|
|
380
531
|
let localStart = Number.isFinite(offsetHint) && (offsetHint ?? 0) >= 0 ? offsetHint : -1;
|
|
381
|
-
|
|
532
|
+
let mappingStatus = "exact";
|
|
533
|
+
if (value && localStart >= 0 && sourceText.slice(localStart, localStart + value.length) !== value) {
|
|
382
534
|
localStart = -1;
|
|
535
|
+
mappingStatus = "fallback";
|
|
536
|
+
}
|
|
383
537
|
if (localStart < 0 || localStart > sourceText.length) {
|
|
384
538
|
localStart = value ? sourceText.indexOf(value, sourceEventCursor) : sourceEventCursor;
|
|
385
539
|
if (localStart < 0) localStart = value ? sourceText.indexOf(value) : sourceEventCursor;
|
|
540
|
+
mappingStatus = "fallback";
|
|
386
541
|
}
|
|
387
542
|
localStart = Math.max(0, localStart);
|
|
388
543
|
const localEnd = Math.min(sourceText.length, localStart + value.length);
|
|
@@ -393,7 +548,8 @@ async function synthesizeSsml(ssml, config) {
|
|
|
393
548
|
return {
|
|
394
549
|
originalTextRange: { ...fallbackRange },
|
|
395
550
|
textRange: { ...fallbackRange },
|
|
396
|
-
...segment ? { sourceNodePath: [...segment.sourceNodePath] } : config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {}
|
|
551
|
+
...segment ? { sourceNodePath: [...segment.sourceNodePath] } : config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
|
|
552
|
+
mappingStatus: segment || config.sourceTextRange || config.sourceNodePath ? mappingStatus : "unmapped"
|
|
397
553
|
};
|
|
398
554
|
};
|
|
399
555
|
synthesizer.wordBoundary = (_sender, event) => {
|
|
@@ -442,20 +598,32 @@ async function synthesizeSsml(ssml, config) {
|
|
|
442
598
|
);
|
|
443
599
|
const durationMs = result.audioDuration ? ticksToMilliseconds(result.audioDuration) : eventDurationMs;
|
|
444
600
|
const requestId = result.resultId;
|
|
445
|
-
const addSourceMetadata = (event) =>
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
601
|
+
const addSourceMetadata = (event) => {
|
|
602
|
+
const mapped = {
|
|
603
|
+
...event,
|
|
604
|
+
...config.sourceTextRange && !("textRange" in event) ? { textRange: { ...config.sourceTextRange } } : {},
|
|
605
|
+
...config.sourceTextRange && !("originalTextRange" in event) ? { originalTextRange: { ...config.sourceTextRange } } : {},
|
|
606
|
+
...config.chunkIndex !== void 0 ? { chunkIndex: config.chunkIndex } : {},
|
|
607
|
+
...config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
|
|
608
|
+
...requestId ? { requestId } : {}
|
|
609
|
+
};
|
|
610
|
+
if (event.mappingStatus === "unmapped") {
|
|
611
|
+
Object.defineProperty(mapped, "mappingStatus", { value: "unmapped", enumerable: false });
|
|
612
|
+
Object.defineProperty(mapped, "toJSON", {
|
|
613
|
+
value: () => ({ ...mapped, mappingStatus: "unmapped" }),
|
|
614
|
+
enumerable: false
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
return mapped;
|
|
618
|
+
};
|
|
453
619
|
const sourceBoundaries = boundaries.map((boundary) => addSourceMetadata(boundary));
|
|
454
620
|
const sourceVisemes = visemes.map((viseme) => addSourceMetadata(viseme));
|
|
455
621
|
const sourceBookmarks = bookmarks.map((bookmark) => addSourceMetadata(bookmark));
|
|
456
622
|
resolve({
|
|
457
623
|
audioData: result.audioData,
|
|
458
624
|
durationMs,
|
|
625
|
+
audioSpec: inspectAudioSpecification(result.audioData, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
|
|
626
|
+
mimeType: resolveMimeType(config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
|
|
459
627
|
...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
|
|
460
628
|
...requestId ? { requestId } : {},
|
|
461
629
|
...sourceBoundaries.length > 0 ? { boundaries: sourceBoundaries, wordBoundary: sourceBoundaries, wordBoundaries: sourceBoundaries } : {},
|
|
@@ -468,10 +636,11 @@ async function synthesizeSsml(ssml, config) {
|
|
|
468
636
|
abortHandler = () => rejectWithError(new SynthesisCancelledError());
|
|
469
637
|
config.signal.addEventListener("abort", abortHandler, { once: true });
|
|
470
638
|
}
|
|
471
|
-
|
|
639
|
+
const timeoutMs = config.timeouts?.perChunkMs ?? config.timeoutMs;
|
|
640
|
+
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
472
641
|
timeout = setTimeout(
|
|
473
|
-
() => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${
|
|
474
|
-
|
|
642
|
+
() => rejectWithError(new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`)),
|
|
643
|
+
timeoutMs
|
|
475
644
|
);
|
|
476
645
|
}
|
|
477
646
|
synthesizer.speakSsmlAsync(ssml, cb, rejectWithError);
|
|
@@ -480,9 +649,109 @@ async function synthesizeSsml(ssml, config) {
|
|
|
480
649
|
}
|
|
481
650
|
});
|
|
482
651
|
}
|
|
652
|
+
function isRetryableSynthesisError(error) {
|
|
653
|
+
if (error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError) return false;
|
|
654
|
+
if (error instanceof AzureTtsError && error.status !== 0)
|
|
655
|
+
return error.status === 429 || error.status >= 500 && error.status < 600;
|
|
656
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
657
|
+
if (/\b4\d{2}\b/.test(message)) return false;
|
|
658
|
+
const status = error && typeof error === "object" && "status" in error ? error.status : void 0;
|
|
659
|
+
if (typeof status === "number") return status === 429 || status >= 500 && status < 600;
|
|
660
|
+
return /network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message);
|
|
661
|
+
}
|
|
662
|
+
function retryDelay(options, retryAttempt, error) {
|
|
663
|
+
const retryAfterMs = getRetryAfterDelayMs(error);
|
|
664
|
+
if (retryAfterMs !== void 0) return retryAfterMs;
|
|
665
|
+
const base = Math.min(options.maxDelayMs, options.initialDelayMs * 2 ** Math.max(0, retryAttempt - 1));
|
|
666
|
+
return Math.floor(Math.random() * (base + 1));
|
|
667
|
+
}
|
|
668
|
+
function resolveConcurrency(value, total) {
|
|
669
|
+
if (value === void 0) return 1;
|
|
670
|
+
if (value === Infinity) return Math.max(1, total);
|
|
671
|
+
return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
|
|
672
|
+
}
|
|
673
|
+
async function waitForRetry(delayMs, signal) {
|
|
674
|
+
if (signal?.aborted) throw new SynthesisCancelledError();
|
|
675
|
+
if (delayMs <= 0) return;
|
|
676
|
+
await new Promise((resolve, reject) => {
|
|
677
|
+
let timer;
|
|
678
|
+
const abort = () => {
|
|
679
|
+
clearTimeout(timer);
|
|
680
|
+
signal?.removeEventListener("abort", abort);
|
|
681
|
+
reject(new SynthesisCancelledError());
|
|
682
|
+
};
|
|
683
|
+
timer = setTimeout(() => {
|
|
684
|
+
signal?.removeEventListener("abort", abort);
|
|
685
|
+
resolve();
|
|
686
|
+
}, delayMs);
|
|
687
|
+
if (signal) {
|
|
688
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
async function synthesizeWithRetry(ssml, config, retryOptions, onRetry) {
|
|
693
|
+
const options = retryOptions ? {
|
|
694
|
+
maxRetries: Math.max(0, Math.floor(retryOptions.maxRetries)),
|
|
695
|
+
initialDelayMs: Math.max(0, retryOptions.initialDelayMs),
|
|
696
|
+
maxDelayMs: Math.max(0, retryOptions.maxDelayMs),
|
|
697
|
+
shouldRetry: retryOptions.shouldRetry
|
|
698
|
+
} : void 0;
|
|
699
|
+
let attempt = 0;
|
|
700
|
+
while (true) {
|
|
701
|
+
if (config.signal?.aborted) throw new SynthesisCancelledError();
|
|
702
|
+
try {
|
|
703
|
+
return await synthesizeSsml(ssml, config);
|
|
704
|
+
} catch (error) {
|
|
705
|
+
if (!options || attempt >= options.maxRetries || !(options.shouldRetry?.(error, attempt + 1) ?? isRetryableSynthesisError(error)))
|
|
706
|
+
throw error;
|
|
707
|
+
attempt += 1;
|
|
708
|
+
const delayMs = retryDelay(options, attempt, error);
|
|
709
|
+
onRetry(attempt, delayMs);
|
|
710
|
+
await waitForRetry(delayMs, config.signal);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
function createAbortScope(parent, timeoutMs) {
|
|
715
|
+
const controller = new AbortController();
|
|
716
|
+
let didTimeout = false;
|
|
717
|
+
const onAbort = () => controller.abort();
|
|
718
|
+
if (parent?.aborted) controller.abort();
|
|
719
|
+
parent?.addEventListener("abort", onAbort, { once: true });
|
|
720
|
+
const timer = timeoutMs !== void 0 && timeoutMs > 0 ? setTimeout(() => {
|
|
721
|
+
didTimeout = true;
|
|
722
|
+
controller.abort();
|
|
723
|
+
}, timeoutMs) : void 0;
|
|
724
|
+
return {
|
|
725
|
+
signal: controller.signal,
|
|
726
|
+
timedOut: () => didTimeout,
|
|
727
|
+
dispose: () => {
|
|
728
|
+
if (timer) clearTimeout(timer);
|
|
729
|
+
parent?.removeEventListener("abort", onAbort);
|
|
730
|
+
},
|
|
731
|
+
abort: () => controller.abort()
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
async function synthesizeChunkWithTimeout(ssml, config, retryOptions, timeoutMs, onRetry) {
|
|
735
|
+
const scope = createAbortScope(config.signal, timeoutMs);
|
|
736
|
+
try {
|
|
737
|
+
return await synthesizeWithRetry(ssml, { ...config, signal: scope.signal }, retryOptions, onRetry);
|
|
738
|
+
} catch (error) {
|
|
739
|
+
if (scope.timedOut()) throw new SynthesisTimeoutError(`Speech synthesis timed out after ${timeoutMs} ms.`);
|
|
740
|
+
throw error;
|
|
741
|
+
} finally {
|
|
742
|
+
scope.dispose();
|
|
743
|
+
}
|
|
744
|
+
}
|
|
483
745
|
async function synthesizeSsmlChunks(chunks, config) {
|
|
484
|
-
const results =
|
|
746
|
+
const results = new Array(chunks.length);
|
|
485
747
|
const totalChunks = chunks.length;
|
|
748
|
+
const cachedChunks = new Map((config.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
|
|
749
|
+
for (const [index, cached] of cachedChunks) {
|
|
750
|
+
if (index >= 0 && index < totalChunks) results[index] = cached;
|
|
751
|
+
}
|
|
752
|
+
const requestedIndices = config.resumeChunkIndices ? new Set(config.resumeChunkIndices.filter((index) => index >= 0 && index < totalChunks)) : void 0;
|
|
753
|
+
const shouldSynthesize = (index) => !cachedChunks.has(index) && (requestedIndices === void 0 || requestedIndices.has(index));
|
|
754
|
+
const scope = createAbortScope(config.signal, config.timeouts?.totalJobMs);
|
|
486
755
|
const report = (event) => config.onProgress?.(event);
|
|
487
756
|
for (const [index, chunk] of chunks.entries()) {
|
|
488
757
|
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
@@ -496,57 +765,112 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
496
765
|
durationMs: 0
|
|
497
766
|
});
|
|
498
767
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
const result = await synthesizeSsml(input.ssml, {
|
|
513
|
-
...config,
|
|
514
|
-
...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
|
|
515
|
-
...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
|
|
516
|
-
...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
|
|
517
|
-
...input.sourceMarkers ? { sourceMarkers: input.sourceMarkers } : {},
|
|
518
|
-
chunkIndex: index,
|
|
519
|
-
onProgress: void 0
|
|
520
|
-
});
|
|
521
|
-
results.push(result);
|
|
522
|
-
report({
|
|
523
|
-
currentChunk: index + 1,
|
|
524
|
-
totalChunks,
|
|
525
|
-
percent: totalChunks === 0 ? 100 : Math.round((index + 1) / totalChunks * 100),
|
|
526
|
-
chunkIndex: index,
|
|
527
|
-
originalTextRange: input.originalTextRange,
|
|
528
|
-
status: "success",
|
|
529
|
-
durationMs: Date.now() - startedAt
|
|
530
|
-
});
|
|
531
|
-
} catch (error) {
|
|
768
|
+
let completed = [...results].filter((result) => result !== void 0).length;
|
|
769
|
+
let nextIndex = 0;
|
|
770
|
+
const concurrency = resolveConcurrency(config.concurrency, chunks.length);
|
|
771
|
+
let firstError;
|
|
772
|
+
const failedIndices = /* @__PURE__ */ new Set();
|
|
773
|
+
const worker = async () => {
|
|
774
|
+
while (true) {
|
|
775
|
+
const index = nextIndex++;
|
|
776
|
+
if (index >= chunks.length) return;
|
|
777
|
+
if (!shouldSynthesize(index)) continue;
|
|
778
|
+
if (firstError && config.cancelOnFailure !== false) return;
|
|
779
|
+
const chunk = chunks[index];
|
|
780
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
532
781
|
report({
|
|
533
|
-
currentChunk:
|
|
782
|
+
currentChunk: completed,
|
|
534
783
|
totalChunks,
|
|
535
|
-
percent: totalChunks === 0 ? 100 : Math.round(
|
|
784
|
+
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
536
785
|
chunkIndex: index,
|
|
537
786
|
originalTextRange: input.originalTextRange,
|
|
538
|
-
status: "
|
|
539
|
-
durationMs:
|
|
540
|
-
error
|
|
787
|
+
status: "synthesizing",
|
|
788
|
+
durationMs: 0
|
|
541
789
|
});
|
|
542
|
-
|
|
790
|
+
const startedAt = Date.now();
|
|
791
|
+
try {
|
|
792
|
+
const result = await synthesizeChunkWithTimeout(
|
|
793
|
+
input.ssml,
|
|
794
|
+
{
|
|
795
|
+
...config,
|
|
796
|
+
signal: scope.signal,
|
|
797
|
+
...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
|
|
798
|
+
...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
|
|
799
|
+
...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
|
|
800
|
+
...input.sourceMarkers ? { sourceMarkers: input.sourceMarkers } : {},
|
|
801
|
+
chunkIndex: index,
|
|
802
|
+
onProgress: void 0
|
|
803
|
+
},
|
|
804
|
+
config.retryOptions,
|
|
805
|
+
config.timeouts?.chunkWithRetriesMs ?? config.timeouts?.perChunkMs ?? config.timeoutMs,
|
|
806
|
+
(retryAttempt, nextRetryDelayMs) => report({
|
|
807
|
+
currentChunk: completed,
|
|
808
|
+
totalChunks,
|
|
809
|
+
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
810
|
+
chunkIndex: index,
|
|
811
|
+
originalTextRange: input.originalTextRange,
|
|
812
|
+
status: "synthesizing",
|
|
813
|
+
durationMs: Date.now() - startedAt,
|
|
814
|
+
retryAttempt,
|
|
815
|
+
nextRetryDelayMs,
|
|
816
|
+
isRetrying: true
|
|
817
|
+
})
|
|
818
|
+
);
|
|
819
|
+
results[index] = result;
|
|
820
|
+
completed += 1;
|
|
821
|
+
report({
|
|
822
|
+
currentChunk: completed,
|
|
823
|
+
totalChunks,
|
|
824
|
+
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
825
|
+
chunkIndex: index,
|
|
826
|
+
originalTextRange: input.originalTextRange,
|
|
827
|
+
status: "success",
|
|
828
|
+
durationMs: Date.now() - startedAt
|
|
829
|
+
});
|
|
830
|
+
} catch (error) {
|
|
831
|
+
failedIndices.add(index);
|
|
832
|
+
report({
|
|
833
|
+
currentChunk: completed,
|
|
834
|
+
totalChunks,
|
|
835
|
+
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
836
|
+
chunkIndex: index,
|
|
837
|
+
originalTextRange: input.originalTextRange,
|
|
838
|
+
status: "failed",
|
|
839
|
+
durationMs: Date.now() - startedAt,
|
|
840
|
+
error
|
|
841
|
+
});
|
|
842
|
+
firstError ?? (firstError = scope.timedOut() ? new SynthesisTimeoutError(`Speech synthesis timed out after ${config.timeouts?.totalJobMs} ms.`) : error);
|
|
843
|
+
if (config.cancelOnFailure !== false) scope.abort();
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
543
846
|
}
|
|
847
|
+
};
|
|
848
|
+
try {
|
|
849
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
850
|
+
if (firstError) throw firstError;
|
|
851
|
+
const orderedResults = results.filter((result) => result !== void 0);
|
|
852
|
+
return await mergeSynthesisResults(orderedResults, {
|
|
853
|
+
format: config.outputFormat ?? DEFAULT_OUTPUT_FORMAT,
|
|
854
|
+
signal: scope.signal,
|
|
855
|
+
customMerger: config.customMerger,
|
|
856
|
+
outputMimeType: config.outputMimeType,
|
|
857
|
+
postMergeValidator: config.postMergeValidator
|
|
858
|
+
});
|
|
859
|
+
} catch (error) {
|
|
860
|
+
const partial = {
|
|
861
|
+
synthesizedChunks: results.flatMap((result, chunkIndex) => result ? [{ ...result, chunkIndex }] : []),
|
|
862
|
+
completedChunks: results.flatMap((result, chunkIndex) => result ? [{ ...result, chunkIndex }] : []),
|
|
863
|
+
pendingChunkIndices: chunks.flatMap((_chunk, chunkIndex) => results[chunkIndex] ? [] : [chunkIndex]),
|
|
864
|
+
failedChunkIndices: [...failedIndices],
|
|
865
|
+
totalChunks
|
|
866
|
+
};
|
|
867
|
+
if (error && typeof error === "object") error.partialResult = partial;
|
|
868
|
+
throw error;
|
|
869
|
+
} finally {
|
|
870
|
+
scope.dispose();
|
|
544
871
|
}
|
|
545
|
-
return mergeSynthesisResults(results, {
|
|
546
|
-
format: config.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3"
|
|
547
|
-
});
|
|
548
872
|
}
|
|
549
|
-
function createMergedResult(results, audioData, format) {
|
|
873
|
+
function createMergedResult(results, audioData, format, audioSpec, outputMimeType) {
|
|
550
874
|
const boundaries = [];
|
|
551
875
|
const visemes = [];
|
|
552
876
|
const bookmarks = [];
|
|
@@ -565,7 +889,8 @@ function createMergedResult(results, audioData, format) {
|
|
|
565
889
|
...boundary.sourceNodePath ? { sourceNodePath: [...boundary.sourceNodePath] } : {},
|
|
566
890
|
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
567
891
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
568
|
-
...requestId ? { requestId } : {}
|
|
892
|
+
...requestId ? { requestId } : {},
|
|
893
|
+
mappingStatus: boundary.mappingStatus ?? "unmapped"
|
|
569
894
|
});
|
|
570
895
|
}
|
|
571
896
|
for (const viseme of result.visemes ?? []) {
|
|
@@ -580,7 +905,8 @@ function createMergedResult(results, audioData, format) {
|
|
|
580
905
|
...viseme.sourceNodePath ? { sourceNodePath: [...viseme.sourceNodePath] } : {},
|
|
581
906
|
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
582
907
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
583
|
-
...requestId ? { requestId } : {}
|
|
908
|
+
...requestId ? { requestId } : {},
|
|
909
|
+
mappingStatus: viseme.mappingStatus ?? "unmapped"
|
|
584
910
|
});
|
|
585
911
|
}
|
|
586
912
|
for (const bookmark of result.bookmarks ?? []) {
|
|
@@ -595,7 +921,8 @@ function createMergedResult(results, audioData, format) {
|
|
|
595
921
|
...bookmark.sourceNodePath ? { sourceNodePath: [...bookmark.sourceNodePath] } : {},
|
|
596
922
|
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
597
923
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
598
|
-
...requestId ? { requestId } : {}
|
|
924
|
+
...requestId ? { requestId } : {},
|
|
925
|
+
mappingStatus: bookmark.mappingStatus ?? "unmapped"
|
|
599
926
|
});
|
|
600
927
|
}
|
|
601
928
|
durationOffset += Math.max(0, result.durationMs);
|
|
@@ -604,6 +931,8 @@ function createMergedResult(results, audioData, format) {
|
|
|
604
931
|
audioData,
|
|
605
932
|
durationMs: durationOffset,
|
|
606
933
|
mimeType: resolveMimeType(format),
|
|
934
|
+
audioSpec: audioSpec ?? formatAudioSpecification(format),
|
|
935
|
+
...outputMimeType ? { mimeType: outputMimeType } : {},
|
|
607
936
|
...boundaries.length > 0 ? { boundaries, wordBoundary: boundaries, wordBoundaries: boundaries } : {},
|
|
608
937
|
...visemes.length > 0 ? { visemes } : {},
|
|
609
938
|
...bookmarks.length > 0 ? { bookmarks } : {},
|
|
@@ -616,19 +945,73 @@ function mergeSynthesisResults(results, options) {
|
|
|
616
945
|
const format = resolvedOptions?.format;
|
|
617
946
|
if (!format) throw new UnsupportedMergeFormatError("");
|
|
618
947
|
const buffers = results.map((result) => result.audioData);
|
|
948
|
+
const inputSpecs = results.map((result) => result.audioSpec ?? inspectAudioSpecification(result.audioData, format));
|
|
949
|
+
validateAudioSpecifications(inputSpecs);
|
|
950
|
+
const signal = resolvedOptions.signal ?? new AbortController().signal;
|
|
951
|
+
if (signal.aborted) throw new SynthesisCancelledError();
|
|
619
952
|
if (resolvedOptions.customMerger) {
|
|
620
|
-
return Promise.resolve().then(
|
|
953
|
+
return Promise.resolve().then(
|
|
954
|
+
() => resolvedOptions.customMerger?.(buffers, {
|
|
955
|
+
format,
|
|
956
|
+
outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
957
|
+
inputSpecs,
|
|
958
|
+
signal
|
|
959
|
+
})
|
|
960
|
+
).then((merged) => {
|
|
621
961
|
if (!merged) throw new MergeError("The custom audio merger returned no audio buffer.");
|
|
622
|
-
|
|
962
|
+
if (!(merged instanceof ArrayBuffer) || buffers.some((buffer) => buffer.byteLength > 0) && merged.byteLength === 0)
|
|
963
|
+
throw new MergeError("The custom audio merger returned an invalid audio buffer.");
|
|
964
|
+
if (signal.aborted) throw new SynthesisCancelledError();
|
|
965
|
+
const result = createMergedResult(
|
|
966
|
+
results,
|
|
967
|
+
merged,
|
|
968
|
+
format,
|
|
969
|
+
inspectAudioSpecification(merged, format),
|
|
970
|
+
resolvedOptions.outputMimeType
|
|
971
|
+
);
|
|
972
|
+
return Promise.resolve(
|
|
973
|
+
resolvedOptions.postMergeValidator?.(result, {
|
|
974
|
+
format,
|
|
975
|
+
outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
976
|
+
inputSpecs,
|
|
977
|
+
signal
|
|
978
|
+
})
|
|
979
|
+
).then((valid) => {
|
|
980
|
+
if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
981
|
+
return result;
|
|
982
|
+
});
|
|
623
983
|
}).catch((error) => {
|
|
624
|
-
if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError)
|
|
984
|
+
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
985
|
+
throw error;
|
|
625
986
|
throw new MergeError(`Custom audio merger failed for format "${format}".`, error);
|
|
626
987
|
});
|
|
627
988
|
}
|
|
628
989
|
try {
|
|
629
|
-
|
|
990
|
+
const result = createMergedResult(
|
|
991
|
+
results,
|
|
992
|
+
mergeAudioBuffers(buffers, { format }),
|
|
993
|
+
format,
|
|
994
|
+
inputSpecs[0],
|
|
995
|
+
resolvedOptions.outputMimeType
|
|
996
|
+
);
|
|
997
|
+
if (resolvedOptions.postMergeValidator) {
|
|
998
|
+
const validation = resolvedOptions.postMergeValidator(result, {
|
|
999
|
+
format,
|
|
1000
|
+
outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
1001
|
+
inputSpecs,
|
|
1002
|
+
signal
|
|
1003
|
+
});
|
|
1004
|
+
if (validation instanceof Promise)
|
|
1005
|
+
return validation.then((valid) => {
|
|
1006
|
+
if (valid === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1007
|
+
return result;
|
|
1008
|
+
});
|
|
1009
|
+
if (validation === false) throw new MergeError("The post-merge validator rejected the merged audio.");
|
|
1010
|
+
}
|
|
1011
|
+
return result;
|
|
630
1012
|
} catch (error) {
|
|
631
|
-
if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError)
|
|
1013
|
+
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
1014
|
+
throw error;
|
|
632
1015
|
throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
|
|
633
1016
|
}
|
|
634
1017
|
}
|
|
@@ -637,7 +1020,10 @@ async function synthesizeSpeech(ssml, config) {
|
|
|
637
1020
|
}
|
|
638
1021
|
|
|
639
1022
|
// src/safe.ts
|
|
640
|
-
import {
|
|
1023
|
+
import {
|
|
1024
|
+
createAzureUrlValidatorRunner,
|
|
1025
|
+
validateAzureSsml
|
|
1026
|
+
} from "@ssml-builder-js/ssml-core";
|
|
641
1027
|
var ChunkValidationError = class extends Error {
|
|
642
1028
|
constructor(chunkIndex, diagnostics) {
|
|
643
1029
|
super(`SSML validation failed for chunk ${chunkIndex}; the Azure Speech API was not called.`);
|
|
@@ -647,11 +1033,125 @@ var ChunkValidationError = class extends Error {
|
|
|
647
1033
|
this.diagnostics = diagnostics;
|
|
648
1034
|
}
|
|
649
1035
|
};
|
|
650
|
-
|
|
651
|
-
|
|
1036
|
+
var BatchChunkValidationError = class extends ChunkValidationError {
|
|
1037
|
+
constructor(chunkDiagnostics) {
|
|
1038
|
+
const first = chunkDiagnostics[0];
|
|
1039
|
+
super(first?.chunkIndex ?? -1, first?.diagnostics ?? []);
|
|
1040
|
+
this.name = "BatchChunkValidationError";
|
|
1041
|
+
this.message = `SSML validation failed for ${chunkDiagnostics.length} chunk(s); the Azure Speech API was not called.`;
|
|
1042
|
+
this.chunkDiagnostics = chunkDiagnostics;
|
|
1043
|
+
this.totalErrorCount = chunkDiagnostics.reduce((total, chunk) => total + chunk.diagnostics.length, 0);
|
|
1044
|
+
this.errorCount = this.totalErrorCount;
|
|
1045
|
+
this.totalErrors = this.totalErrorCount;
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
function failure(error, partialResult) {
|
|
1049
|
+
return {
|
|
1050
|
+
ok: false,
|
|
1051
|
+
success: false,
|
|
1052
|
+
status: error.kind,
|
|
1053
|
+
error,
|
|
1054
|
+
...partialResult ? { partialResult } : {}
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
function partialResultFrom(error) {
|
|
1058
|
+
if (!error || typeof error !== "object") return void 0;
|
|
1059
|
+
const partial = error.partialResult;
|
|
1060
|
+
if (!partial || typeof partial !== "object") return void 0;
|
|
1061
|
+
return partial;
|
|
1062
|
+
}
|
|
1063
|
+
function createSafeAbortScope(parent, timeoutMs) {
|
|
1064
|
+
const controller = new AbortController();
|
|
1065
|
+
let didTimeout = false;
|
|
1066
|
+
const onAbort = () => controller.abort();
|
|
1067
|
+
if (parent?.aborted) controller.abort();
|
|
1068
|
+
parent?.addEventListener("abort", onAbort, { once: true });
|
|
1069
|
+
const timer = timeoutMs !== void 0 && timeoutMs > 0 ? setTimeout(() => {
|
|
1070
|
+
didTimeout = true;
|
|
1071
|
+
controller.abort();
|
|
1072
|
+
}, timeoutMs) : void 0;
|
|
1073
|
+
return {
|
|
1074
|
+
signal: controller.signal,
|
|
1075
|
+
timedOut: () => didTimeout,
|
|
1076
|
+
dispose: () => {
|
|
1077
|
+
if (timer) clearTimeout(timer);
|
|
1078
|
+
parent?.removeEventListener("abort", onAbort);
|
|
1079
|
+
},
|
|
1080
|
+
abort: () => controller.abort()
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
function isRetryable(error) {
|
|
1084
|
+
if (error instanceof Error && /cancel|abort|tim(?:e|ed) ?out/i.test(error.message)) return false;
|
|
1085
|
+
const status = error && typeof error === "object" && "status" in error ? error.status : void 0;
|
|
1086
|
+
if (typeof status === "number" && status !== 0) return status === 429 || status >= 500 && status < 600;
|
|
1087
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1088
|
+
if (/\b4\d{2}\b/.test(message)) return false;
|
|
1089
|
+
return /network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message);
|
|
1090
|
+
}
|
|
1091
|
+
function delayForRetry(options, attempt) {
|
|
1092
|
+
const maxDelay = Math.max(0, options.maxDelayMs);
|
|
1093
|
+
const base = Math.min(maxDelay, Math.max(0, options.initialDelayMs) * 2 ** Math.max(0, attempt - 1));
|
|
1094
|
+
return Math.floor(Math.random() * (base + 1));
|
|
1095
|
+
}
|
|
1096
|
+
function retryDelayForError(options, attempt, error) {
|
|
1097
|
+
return getRetryAfterDelayMs(error) ?? delayForRetry(options, attempt);
|
|
1098
|
+
}
|
|
1099
|
+
function resolveConcurrency2(value, total) {
|
|
1100
|
+
if (value === void 0) return 1;
|
|
1101
|
+
if (value === Infinity) return Math.max(1, total);
|
|
1102
|
+
return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
|
|
1103
|
+
}
|
|
1104
|
+
async function retryableSynthesis(synthesize, options, signal, onRetry) {
|
|
1105
|
+
const retry = options ? {
|
|
1106
|
+
maxRetries: Math.max(0, Math.floor(options.maxRetries)),
|
|
1107
|
+
initialDelayMs: options.initialDelayMs,
|
|
1108
|
+
maxDelayMs: options.maxDelayMs,
|
|
1109
|
+
shouldRetry: options.shouldRetry
|
|
1110
|
+
} : void 0;
|
|
1111
|
+
let attempt = 0;
|
|
1112
|
+
while (true) {
|
|
1113
|
+
if (signal?.aborted) throw new Error("Speech synthesis was cancelled.");
|
|
1114
|
+
try {
|
|
1115
|
+
return await synthesize();
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
if (!retry || attempt >= retry.maxRetries || !(retry.shouldRetry?.(error, attempt + 1) ?? isRetryable(error)))
|
|
1118
|
+
throw error;
|
|
1119
|
+
attempt += 1;
|
|
1120
|
+
const delayMs = retryDelayForError(retry, attempt, error);
|
|
1121
|
+
onRetry(attempt, delayMs);
|
|
1122
|
+
if (delayMs > 0)
|
|
1123
|
+
await new Promise((resolve, reject) => {
|
|
1124
|
+
const timer = setTimeout(() => {
|
|
1125
|
+
signal?.removeEventListener("abort", abort);
|
|
1126
|
+
resolve();
|
|
1127
|
+
}, delayMs);
|
|
1128
|
+
const abort = () => {
|
|
1129
|
+
clearTimeout(timer);
|
|
1130
|
+
signal?.removeEventListener("abort", abort);
|
|
1131
|
+
reject(new Error("Speech synthesis was cancelled."));
|
|
1132
|
+
};
|
|
1133
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
function sharedValidationOptions(options, signal) {
|
|
1139
|
+
const validator = options.urlValidator ?? options.customUrlValidator;
|
|
1140
|
+
if (!validator) return signal ? withValidationSignal(options, signal) : options;
|
|
1141
|
+
const runner = createAzureUrlValidatorRunner(validator, {
|
|
1142
|
+
...options.urlValidation ?? {},
|
|
1143
|
+
...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
|
|
1144
|
+
...options.timeouts?.urlValidationMs !== void 0 ? { timeoutMs: options.timeouts.urlValidationMs } : options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
|
|
1145
|
+
...signal ? { signal } : {},
|
|
1146
|
+
...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
|
|
1147
|
+
});
|
|
1148
|
+
return {
|
|
1149
|
+
...withValidationSignal(options, signal),
|
|
1150
|
+
urlValidatorRunner: runner
|
|
1151
|
+
};
|
|
652
1152
|
}
|
|
653
1153
|
async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
654
|
-
const validationOptions =
|
|
1154
|
+
const validationOptions = sharedValidationOptions(options.validation ?? options, options.signal);
|
|
655
1155
|
const diagnostics = await Promise.resolve(validateAzureSsml(ssml, validationOptions));
|
|
656
1156
|
if (options.signal?.aborted) {
|
|
657
1157
|
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
@@ -670,7 +1170,11 @@ async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
|
670
1170
|
ok: true,
|
|
671
1171
|
success: true,
|
|
672
1172
|
status: "success",
|
|
673
|
-
value: await client.synthesizeSsml(ssml, {
|
|
1173
|
+
value: await client.synthesizeSsml(ssml, {
|
|
1174
|
+
signal: options.signal,
|
|
1175
|
+
timeoutMs: options.timeouts?.perChunkMs,
|
|
1176
|
+
timeouts: options.timeouts
|
|
1177
|
+
})
|
|
674
1178
|
};
|
|
675
1179
|
} catch (error) {
|
|
676
1180
|
const synthesisError = toSynthesisError(error);
|
|
@@ -678,7 +1182,10 @@ async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
|
678
1182
|
}
|
|
679
1183
|
}
|
|
680
1184
|
async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
681
|
-
const validationOptions =
|
|
1185
|
+
const validationOptions = sharedValidationOptions(
|
|
1186
|
+
{ ...options.validation ?? options, timeouts: options.timeouts },
|
|
1187
|
+
options.signal
|
|
1188
|
+
);
|
|
682
1189
|
if (options.signal?.aborted) {
|
|
683
1190
|
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
684
1191
|
return failure(error);
|
|
@@ -699,21 +1206,30 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
699
1206
|
pending(index, "pending");
|
|
700
1207
|
});
|
|
701
1208
|
const validations = await Promise.all(
|
|
702
|
-
chunks.map(async (chunk) => {
|
|
1209
|
+
chunks.map(async (chunk, index) => {
|
|
703
1210
|
const ssml = typeof chunk === "string" ? chunk : chunk.ssml;
|
|
704
1211
|
const sourceNodePath = typeof chunk === "string" ? options.sourceNodePath : chunk.sourceNodePath ?? options.sourceNodePath;
|
|
705
1212
|
const diagnostics = await Promise.resolve(
|
|
706
|
-
validateAzureSsml(ssml, {
|
|
1213
|
+
validateAzureSsml(ssml, {
|
|
1214
|
+
...validationOptions,
|
|
1215
|
+
...sourceNodePath ? { sourceNodePath } : {},
|
|
1216
|
+
chunkIndex: index
|
|
1217
|
+
})
|
|
707
1218
|
);
|
|
708
1219
|
return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
709
1220
|
})
|
|
710
1221
|
);
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
const error = new ChunkValidationError(firstInvalidIndex, validations[firstInvalidIndex] ?? []);
|
|
714
|
-
pending(firstInvalidIndex, "failed", error);
|
|
1222
|
+
if (options.signal?.aborted) {
|
|
1223
|
+
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
715
1224
|
return failure(error);
|
|
716
1225
|
}
|
|
1226
|
+
const chunkDiagnostics = validations.map((diagnostics, chunkIndex) => ({ chunkIndex, diagnostics })).filter((entry) => entry.diagnostics.length > 0);
|
|
1227
|
+
if (chunkDiagnostics.length > 0) {
|
|
1228
|
+
const error = new BatchChunkValidationError(chunkDiagnostics);
|
|
1229
|
+
for (const entry of chunkDiagnostics) pending(entry.chunkIndex, "failed", error);
|
|
1230
|
+
return failure(error);
|
|
1231
|
+
}
|
|
1232
|
+
let fallbackJobScope;
|
|
717
1233
|
try {
|
|
718
1234
|
if (client.synthesizeChunks) {
|
|
719
1235
|
const normalizedChunks = chunks.map((chunk) => {
|
|
@@ -725,101 +1241,181 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
725
1241
|
outputFormat: options.outputFormat,
|
|
726
1242
|
signal: options.signal,
|
|
727
1243
|
timeoutMs: options.timeoutMs,
|
|
728
|
-
|
|
1244
|
+
timeouts: options.timeouts,
|
|
1245
|
+
sourceNodePath: options.sourceNodePath,
|
|
1246
|
+
concurrency: options.concurrency,
|
|
1247
|
+
retryOptions: options.retryOptions,
|
|
1248
|
+
cancelOnFailure: options.cancelOnFailure,
|
|
1249
|
+
resumeChunks: options.resumeChunks,
|
|
1250
|
+
resumeChunkIndices: options.resumeChunkIndices,
|
|
1251
|
+
customMerger: options.customMerger,
|
|
1252
|
+
outputMimeType: options.outputMimeType,
|
|
1253
|
+
postMergeValidator: options.postMergeValidator
|
|
729
1254
|
});
|
|
730
1255
|
return { ok: true, success: true, status: "success", value };
|
|
731
1256
|
}
|
|
732
|
-
const results =
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
1257
|
+
const results = new Array(chunks.length);
|
|
1258
|
+
const cachedChunks = new Map((options.resumeChunks ?? []).map((chunk) => [chunk.chunkIndex, chunk]));
|
|
1259
|
+
for (const [index, cached] of cachedChunks) {
|
|
1260
|
+
if (index >= 0 && index < chunks.length) results[index] = cached;
|
|
1261
|
+
}
|
|
1262
|
+
const requestedIndices = options.resumeChunkIndices ? new Set(options.resumeChunkIndices.filter((index) => index >= 0 && index < chunks.length)) : void 0;
|
|
1263
|
+
const shouldSynthesize = (index) => !cachedChunks.has(index) && (requestedIndices === void 0 || requestedIndices.has(index));
|
|
1264
|
+
const jobScope = chunks.length > 1 || options.timeouts?.totalJobMs !== void 0 ? createSafeAbortScope(options.signal, options.timeouts?.totalJobMs) : void 0;
|
|
1265
|
+
fallbackJobScope = jobScope;
|
|
1266
|
+
const failedIndices = /* @__PURE__ */ new Set();
|
|
1267
|
+
let firstError;
|
|
1268
|
+
let completed = [...results].filter((result) => result !== void 0).length;
|
|
1269
|
+
let nextIndex = 0;
|
|
1270
|
+
const concurrency = resolveConcurrency2(options.concurrency, chunks.length);
|
|
1271
|
+
const worker = async () => {
|
|
1272
|
+
while (true) {
|
|
1273
|
+
const index = nextIndex++;
|
|
1274
|
+
if (index >= chunks.length) return;
|
|
1275
|
+
if (!shouldSynthesize(index)) continue;
|
|
1276
|
+
if (failedIndices.size > 0 && options.cancelOnFailure !== false) return;
|
|
1277
|
+
const chunk = chunks[index];
|
|
1278
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
1279
|
+
const sourceNodePath = input.sourceNodePath;
|
|
1280
|
+
const originalTextRange = input.originalTextRange;
|
|
1281
|
+
pending(index, "synthesizing");
|
|
1282
|
+
const startedAt = Date.now();
|
|
1283
|
+
try {
|
|
1284
|
+
const chunkTimeout = options.timeouts?.chunkWithRetriesMs ?? options.timeouts?.perChunkMs;
|
|
1285
|
+
const chunkScope = chunkTimeout !== void 0 || jobScope ? createSafeAbortScope(jobScope?.signal ?? options.signal, chunkTimeout ?? options.timeoutMs) : void 0;
|
|
1286
|
+
const chunkSignal = chunkScope?.signal ?? options.signal;
|
|
1287
|
+
let result;
|
|
1288
|
+
try {
|
|
1289
|
+
result = await retryableSynthesis(
|
|
1290
|
+
() => client.synthesizeSsml(input.ssml, {
|
|
1291
|
+
outputFormat: options.outputFormat,
|
|
1292
|
+
signal: chunkSignal,
|
|
1293
|
+
timeoutMs: options.timeouts?.perChunkMs ?? options.timeoutMs,
|
|
1294
|
+
sourceNodePath: input.sourceNodePath ?? options.sourceNodePath
|
|
1295
|
+
}),
|
|
1296
|
+
options.retryOptions,
|
|
1297
|
+
chunkSignal,
|
|
1298
|
+
(retryAttempt, nextRetryDelayMs) => options.onProgress?.({
|
|
1299
|
+
currentChunk: completed,
|
|
1300
|
+
totalChunks: chunks.length,
|
|
1301
|
+
percent: chunks.length === 0 ? 100 : Math.round(completed / chunks.length * 100),
|
|
1302
|
+
chunkIndex: index,
|
|
1303
|
+
originalTextRange: input.originalTextRange,
|
|
1304
|
+
status: "synthesizing",
|
|
1305
|
+
durationMs: Date.now() - startedAt,
|
|
1306
|
+
retryAttempt,
|
|
1307
|
+
nextRetryDelayMs,
|
|
1308
|
+
isRetrying: true
|
|
1309
|
+
})
|
|
1310
|
+
);
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
if (chunkScope?.timedOut())
|
|
1313
|
+
throw new Error(`Speech synthesis timed out after ${chunkTimeout ?? options.timeoutMs} ms.`);
|
|
1314
|
+
throw error;
|
|
1315
|
+
} finally {
|
|
1316
|
+
chunkScope?.dispose();
|
|
1317
|
+
}
|
|
1318
|
+
results[index] = {
|
|
1319
|
+
...result,
|
|
1320
|
+
...input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {},
|
|
1321
|
+
...sourceNodePath ? {
|
|
1322
|
+
boundaries: result.boundaries?.map((event) => ({
|
|
1323
|
+
...event,
|
|
1324
|
+
sourceNodePath: [...sourceNodePath],
|
|
1325
|
+
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
1326
|
+
})),
|
|
1327
|
+
visemes: result.visemes?.map((event) => ({
|
|
1328
|
+
...event,
|
|
1329
|
+
sourceNodePath: [...sourceNodePath],
|
|
1330
|
+
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
1331
|
+
})),
|
|
1332
|
+
bookmarks: result.bookmarks?.map((event) => ({
|
|
1333
|
+
...event,
|
|
1334
|
+
sourceNodePath: [...sourceNodePath],
|
|
1335
|
+
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
1336
|
+
}))
|
|
1337
|
+
} : {},
|
|
1338
|
+
...originalTextRange ? {
|
|
1339
|
+
boundaries: result.boundaries?.map((event) => ({
|
|
1340
|
+
...event,
|
|
1341
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1342
|
+
})),
|
|
1343
|
+
wordBoundary: result.wordBoundary?.map((event) => ({
|
|
1344
|
+
...event,
|
|
1345
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1346
|
+
})),
|
|
1347
|
+
wordBoundaries: result.wordBoundaries?.map((event) => ({
|
|
1348
|
+
...event,
|
|
1349
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1350
|
+
})),
|
|
1351
|
+
visemes: result.visemes?.map((event) => ({
|
|
1352
|
+
...event,
|
|
1353
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1354
|
+
})),
|
|
1355
|
+
bookmarks: result.bookmarks?.map((event) => ({
|
|
1356
|
+
...event,
|
|
1357
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1358
|
+
}))
|
|
1359
|
+
} : {}
|
|
1360
|
+
};
|
|
1361
|
+
completed += 1;
|
|
1362
|
+
options.onProgress?.({
|
|
1363
|
+
currentChunk: completed,
|
|
1364
|
+
totalChunks: chunks.length,
|
|
1365
|
+
percent: chunks.length === 0 ? 100 : Math.round(completed / chunks.length * 100),
|
|
1366
|
+
chunkIndex: index,
|
|
1367
|
+
originalTextRange: input.originalTextRange,
|
|
1368
|
+
status: "success",
|
|
1369
|
+
durationMs: Date.now() - startedAt
|
|
1370
|
+
});
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
failedIndices.add(index);
|
|
1373
|
+
options.onProgress?.({
|
|
1374
|
+
currentChunk: completed,
|
|
1375
|
+
totalChunks: chunks.length,
|
|
1376
|
+
percent: chunks.length === 0 ? 100 : Math.round(index / chunks.length * 100),
|
|
1377
|
+
chunkIndex: index,
|
|
1378
|
+
originalTextRange: input.originalTextRange,
|
|
1379
|
+
status: "failed",
|
|
1380
|
+
durationMs: Date.now() - startedAt,
|
|
1381
|
+
error
|
|
1382
|
+
});
|
|
1383
|
+
if (options.cancelOnFailure !== false) jobScope?.abort();
|
|
1384
|
+
firstError ?? (firstError = error);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
810
1387
|
}
|
|
1388
|
+
};
|
|
1389
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
1390
|
+
if (failedIndices.size > 0) {
|
|
1391
|
+
const error = firstError ?? new Error("One or more SSML chunks failed to synthesize.");
|
|
1392
|
+
error.partialResult = {
|
|
1393
|
+
synthesizedChunks: results.flatMap((result, chunkIndex) => result ? [{ ...result, chunkIndex }] : []),
|
|
1394
|
+
completedChunks: results.flatMap((result, chunkIndex) => result ? [{ ...result, chunkIndex }] : []),
|
|
1395
|
+
pendingChunkIndices: chunks.flatMap((_chunk, chunkIndex) => results[chunkIndex] ? [] : [chunkIndex]),
|
|
1396
|
+
failedChunkIndices: [...failedIndices],
|
|
1397
|
+
totalChunks: chunks.length
|
|
1398
|
+
};
|
|
1399
|
+
throw error;
|
|
811
1400
|
}
|
|
1401
|
+
const orderedResults = results.filter((result) => result !== void 0);
|
|
812
1402
|
return {
|
|
813
1403
|
ok: true,
|
|
814
1404
|
success: true,
|
|
815
1405
|
status: "success",
|
|
816
|
-
value: mergeSynthesisResults(
|
|
817
|
-
format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3"
|
|
1406
|
+
value: await mergeSynthesisResults(orderedResults, {
|
|
1407
|
+
format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3",
|
|
1408
|
+
signal: jobScope?.signal ?? options.signal,
|
|
1409
|
+
customMerger: options.customMerger,
|
|
1410
|
+
outputMimeType: options.outputMimeType,
|
|
1411
|
+
postMergeValidator: options.postMergeValidator
|
|
818
1412
|
})
|
|
819
1413
|
};
|
|
820
1414
|
} catch (error) {
|
|
821
1415
|
const synthesisError = toSynthesisError(error);
|
|
822
|
-
return failure(synthesisError);
|
|
1416
|
+
return failure(synthesisError, partialResultFrom(error));
|
|
1417
|
+
} finally {
|
|
1418
|
+
fallbackJobScope?.dispose();
|
|
823
1419
|
}
|
|
824
1420
|
}
|
|
825
1421
|
function withValidationSignal(options, signal) {
|
|
@@ -840,14 +1436,14 @@ var AzureTtsClient = class {
|
|
|
840
1436
|
__privateSet(this, _options, options);
|
|
841
1437
|
}
|
|
842
1438
|
async synthesize(ssml) {
|
|
843
|
-
const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
|
|
1439
|
+
const { region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts } = __privateGet(this, _options);
|
|
844
1440
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
845
1441
|
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
846
|
-
const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs };
|
|
1442
|
+
const config = { endpoint, region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts };
|
|
847
1443
|
return synthesizeSpeech(ssml, config);
|
|
848
1444
|
}
|
|
849
1445
|
async synthesizeSsml(ssml, options = {}) {
|
|
850
|
-
const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
|
|
1446
|
+
const { region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts } = __privateGet(this, _options);
|
|
851
1447
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
852
1448
|
__privateGet(this, _options).logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
853
1449
|
return synthesizeSsml(ssml, {
|
|
@@ -857,13 +1453,14 @@ var AzureTtsClient = class {
|
|
|
857
1453
|
outputFormat: options.outputFormat ?? outputFormat,
|
|
858
1454
|
signal: options.signal ?? signal,
|
|
859
1455
|
timeoutMs: options.timeoutMs ?? timeoutMs,
|
|
1456
|
+
timeouts: options.timeouts ?? timeouts,
|
|
860
1457
|
sourceNodePath: options.sourceNodePath,
|
|
861
1458
|
sourceTextSegments: options.sourceTextSegments,
|
|
862
1459
|
sourceMarkers: options.sourceMarkers
|
|
863
1460
|
});
|
|
864
1461
|
}
|
|
865
1462
|
async synthesizeChunks(chunks, options = {}) {
|
|
866
|
-
const { region, subscriptionKey, outputFormat, signal, timeoutMs } = __privateGet(this, _options);
|
|
1463
|
+
const { region, subscriptionKey, outputFormat, signal, timeoutMs, timeouts } = __privateGet(this, _options);
|
|
867
1464
|
const endpoint = __privateGet(this, _options).endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
868
1465
|
return synthesizeSsmlChunks(chunks, {
|
|
869
1466
|
endpoint,
|
|
@@ -872,8 +1469,17 @@ var AzureTtsClient = class {
|
|
|
872
1469
|
outputFormat: options.outputFormat ?? outputFormat,
|
|
873
1470
|
signal: options.signal ?? signal,
|
|
874
1471
|
timeoutMs: options.timeoutMs ?? timeoutMs,
|
|
1472
|
+
timeouts: options.timeouts ?? timeouts,
|
|
875
1473
|
sourceNodePath: options.sourceNodePath,
|
|
876
|
-
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
|
|
1474
|
+
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
1475
|
+
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
1476
|
+
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions,
|
|
1477
|
+
cancelOnFailure: options.cancelOnFailure,
|
|
1478
|
+
resumeChunks: options.resumeChunks,
|
|
1479
|
+
resumeChunkIndices: options.resumeChunkIndices,
|
|
1480
|
+
customMerger: options.customMerger,
|
|
1481
|
+
outputMimeType: options.outputMimeType,
|
|
1482
|
+
postMergeValidator: options.postMergeValidator
|
|
877
1483
|
});
|
|
878
1484
|
}
|
|
879
1485
|
async synthesizeSsmlSafe(ssml, options = {}) {
|
|
@@ -885,7 +1491,10 @@ var AzureTtsClient = class {
|
|
|
885
1491
|
outputFormat: options.outputFormat ?? __privateGet(this, _options).outputFormat,
|
|
886
1492
|
signal: options.signal ?? __privateGet(this, _options).signal,
|
|
887
1493
|
timeoutMs: options.timeoutMs ?? __privateGet(this, _options).timeoutMs,
|
|
888
|
-
|
|
1494
|
+
timeouts: options.timeouts ?? __privateGet(this, _options).timeouts,
|
|
1495
|
+
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
1496
|
+
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
1497
|
+
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions
|
|
889
1498
|
});
|
|
890
1499
|
}
|
|
891
1500
|
async synthesizeSsmlChunksSafe(chunks, options = {}) {
|
|
@@ -976,14 +1585,18 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
976
1585
|
voiceCount: sortedVoices.length,
|
|
977
1586
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
978
1587
|
apiVersion: AZURE_VOICE_API_VERSION,
|
|
979
|
-
regions
|
|
1588
|
+
regions,
|
|
1589
|
+
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3).toISOString(),
|
|
1590
|
+
regionDiffs: {}
|
|
980
1591
|
}
|
|
981
1592
|
};
|
|
982
1593
|
}
|
|
983
1594
|
export {
|
|
1595
|
+
AudioFormatMismatchError,
|
|
984
1596
|
AzureTtsClient,
|
|
985
1597
|
AzureTtsError,
|
|
986
1598
|
AzureTtsSdkError,
|
|
1599
|
+
BatchChunkValidationError,
|
|
987
1600
|
ChunkValidationError,
|
|
988
1601
|
DEFAULT_OUTPUT_FORMAT,
|
|
989
1602
|
MergeError,
|
|
@@ -992,6 +1605,8 @@ export {
|
|
|
992
1605
|
UnsupportedMergeFormatError,
|
|
993
1606
|
canMergeAudioFormat,
|
|
994
1607
|
fetchAzureVoiceCatalog,
|
|
1608
|
+
getRetryAfterDelayMs,
|
|
1609
|
+
inspectAudioSpecification,
|
|
995
1610
|
mergeAudioBuffers,
|
|
996
1611
|
mergeSynthesisResults,
|
|
997
1612
|
resolveMergeAudioFormat,
|