@ssml-builder-js/azure-tts-client 2.15.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/dist/index.d.mts +52 -5
- package/dist/index.d.ts +52 -5
- package/dist/index.js +510 -154
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +512 -155
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +4 -0
- package/src/errors.ts +23 -1
- package/src/index.ts +12 -1
- package/src/safe.ts +238 -111
- package/src/synthesis.ts +361 -66
- package/src/types.ts +31 -0
- package/test/v216-pipeline.test.ts +110 -0
package/dist/index.mjs
CHANGED
|
@@ -48,6 +48,14 @@ var MergeError = class extends Error {
|
|
|
48
48
|
this.cause = cause;
|
|
49
49
|
}
|
|
50
50
|
};
|
|
51
|
+
var AudioFormatMismatchError = class extends Error {
|
|
52
|
+
constructor(message, inputSpecs = []) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.kind = "audio-format-mismatch";
|
|
55
|
+
this.name = "AudioFormatMismatchError";
|
|
56
|
+
this.inputSpecs = inputSpecs;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
51
59
|
var UnsupportedMergeFormatError = class extends Error {
|
|
52
60
|
constructor(format) {
|
|
53
61
|
super(`Audio format "${format}" cannot be safely concatenated; container re-multiplexing is required.`);
|
|
@@ -57,7 +65,7 @@ var UnsupportedMergeFormatError = class extends Error {
|
|
|
57
65
|
}
|
|
58
66
|
};
|
|
59
67
|
function toSynthesisError(error) {
|
|
60
|
-
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
|
|
68
|
+
if (error instanceof AzureTtsError || error instanceof MergeError || error instanceof AudioFormatMismatchError || error instanceof UnsupportedMergeFormatError || error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError)
|
|
61
69
|
return error;
|
|
62
70
|
const message = error instanceof Error ? error.message : String(error);
|
|
63
71
|
if (/cancel|abort/i.test(message)) return new SynthesisCancelledError(message);
|
|
@@ -187,6 +195,105 @@ function parseWav(buffer) {
|
|
|
187
195
|
}
|
|
188
196
|
return { chunks, data, format };
|
|
189
197
|
}
|
|
198
|
+
function formatNumber(format, pattern, fallback) {
|
|
199
|
+
const match = pattern.exec(format);
|
|
200
|
+
return match?.[1] ? Number(match[1]) : fallback;
|
|
201
|
+
}
|
|
202
|
+
function formatChannels(format, fallback) {
|
|
203
|
+
if (/stereo|2ch|dual/i.test(format)) return 2;
|
|
204
|
+
if (/mono|1ch/i.test(format)) return 1;
|
|
205
|
+
return fallback;
|
|
206
|
+
}
|
|
207
|
+
function formatAudioSpecification(format) {
|
|
208
|
+
const sampleRate = formatNumber(format, /(\d+)(?:khz|kHz|hz|Hz)/, 0);
|
|
209
|
+
const channels = formatChannels(format, 0);
|
|
210
|
+
const bitrateMatch = /(\d+)(?:kbitrate|kbps|kbits?)/i.exec(format);
|
|
211
|
+
const bitrate = bitrateMatch?.[1] ? Number(bitrateMatch[1]) * 1e3 : void 0;
|
|
212
|
+
const codec = /mp3|mpeg/i.test(format) ? "mp3" : /opus/i.test(format) ? "opus" : /silk/i.test(format) ? "silk" : /pcm|mulaw|alaw|siren/i.test(format) ? "pcm" : "unknown";
|
|
213
|
+
return {
|
|
214
|
+
format,
|
|
215
|
+
mimeType: resolveMimeType(format),
|
|
216
|
+
codec,
|
|
217
|
+
sampleRate,
|
|
218
|
+
channels,
|
|
219
|
+
...bitrate ? { bitrate } : {},
|
|
220
|
+
isCompressed: codec === "mp3" || codec === "opus" || codec === "silk"
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function parseMp3Specification(buffer, format) {
|
|
224
|
+
const bytes = stripMp3Tags(buffer);
|
|
225
|
+
const bitrates = [
|
|
226
|
+
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
227
|
+
[0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0],
|
|
228
|
+
[0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0]
|
|
229
|
+
];
|
|
230
|
+
const sampleRates = [
|
|
231
|
+
[44100, 48e3, 32e3],
|
|
232
|
+
[22050, 24e3, 16e3],
|
|
233
|
+
[11025, 12e3, 8e3]
|
|
234
|
+
];
|
|
235
|
+
for (let index = 0; index + 4 <= bytes.length; index += 1) {
|
|
236
|
+
if (bytes[index] !== 255 || (bytes[index + 1] ?? 0) < 224) continue;
|
|
237
|
+
const header = bytes[index + 1] ?? 0;
|
|
238
|
+
const versionBits = header >> 3 & 3;
|
|
239
|
+
const layer = header >> 1 & 3;
|
|
240
|
+
const bitrateIndex = (bytes[index + 2] ?? 0) >> 4;
|
|
241
|
+
const sampleIndex = (bytes[index + 2] ?? 0) >> 2 & 3;
|
|
242
|
+
if (versionBits === 1 || layer !== 1 || bitrateIndex === 0 || bitrateIndex === 15 || sampleIndex === 3) continue;
|
|
243
|
+
const versionIndex = versionBits === 3 ? 0 : versionBits === 2 ? 1 : 2;
|
|
244
|
+
const bitrateTable = versionBits === 3 ? bitrates[1] : bitrates[2];
|
|
245
|
+
const sampleRate = sampleRates[versionIndex]?.[sampleIndex] ?? 0;
|
|
246
|
+
const bitrateKbps = bitrateTable?.[bitrateIndex] ?? 0;
|
|
247
|
+
if (!sampleRate || !bitrateKbps) continue;
|
|
248
|
+
return {
|
|
249
|
+
format,
|
|
250
|
+
mimeType: "audio/mpeg",
|
|
251
|
+
codec: "mp3",
|
|
252
|
+
sampleRate,
|
|
253
|
+
channels: (bytes[index + 3] ?? 0) >> 6 === 3 ? 1 : 2,
|
|
254
|
+
bitrate: bitrateKbps * 1e3,
|
|
255
|
+
isCompressed: true
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return void 0;
|
|
259
|
+
}
|
|
260
|
+
function inspectAudioSpecification(buffer, format) {
|
|
261
|
+
if (isWavFormat(format) || ascii(new Uint8Array(buffer), 0, "RIFF")) {
|
|
262
|
+
const parsed = parseWav(buffer);
|
|
263
|
+
if (parsed.format.byteLength < 16) throw new Error("Invalid WAV fmt chunk.");
|
|
264
|
+
const view = new DataView(parsed.format.buffer, parsed.format.byteOffset, parsed.format.byteLength);
|
|
265
|
+
const sampleRate = view.getUint32(4, true);
|
|
266
|
+
const channels = view.getUint16(2, true);
|
|
267
|
+
const bitsPerSample = parsed.format.byteLength >= 16 ? view.getUint16(14, true) : 0;
|
|
268
|
+
const formatCode = view.getUint16(0, true);
|
|
269
|
+
return {
|
|
270
|
+
format,
|
|
271
|
+
mimeType: "audio/wav",
|
|
272
|
+
codec: formatCode === 1 ? "pcm" : "unknown",
|
|
273
|
+
sampleRate,
|
|
274
|
+
channels,
|
|
275
|
+
...sampleRate && channels && bitsPerSample ? { bitrate: sampleRate * channels * bitsPerSample } : {},
|
|
276
|
+
isCompressed: formatCode !== 1
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (isMp3Format(format)) return parseMp3Specification(buffer, format) ?? formatAudioSpecification(format);
|
|
280
|
+
return formatAudioSpecification(format);
|
|
281
|
+
}
|
|
282
|
+
function validateAudioSpecifications(specs) {
|
|
283
|
+
const first = specs[0];
|
|
284
|
+
if (!first) return;
|
|
285
|
+
const mismatch = specs.find(
|
|
286
|
+
(spec) => spec.sampleRate !== first.sampleRate || spec.channels !== first.channels || first.bitrate !== void 0 && spec.bitrate !== void 0 && spec.bitrate !== first.bitrate
|
|
287
|
+
);
|
|
288
|
+
if (mismatch)
|
|
289
|
+
throw new AudioFormatMismatchError(
|
|
290
|
+
`Audio chunks have incompatible specifications: ${first.sampleRate}Hz/${first.channels}ch versus ${mismatch.sampleRate}Hz/${mismatch.channels}ch.`,
|
|
291
|
+
specs
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
function isAudioFormatMismatch(error) {
|
|
295
|
+
return error instanceof AudioFormatMismatchError || error !== null && typeof error === "object" && "kind" in error && error.kind === "audio-format-mismatch";
|
|
296
|
+
}
|
|
190
297
|
function writeUint32(target, offset, value) {
|
|
191
298
|
new DataView(target.buffer).setUint32(offset, value, true);
|
|
192
299
|
}
|
|
@@ -272,6 +379,7 @@ function mergeAudioBuffers(buffers, options) {
|
|
|
272
379
|
const format = typeof options === "string" ? options : options?.format;
|
|
273
380
|
if (!format) throw new UnsupportedMergeFormatError("");
|
|
274
381
|
try {
|
|
382
|
+
validateAudioSpecifications(buffers.map((buffer) => inspectAudioSpecification(buffer, format)));
|
|
275
383
|
if (isWavFormat(format)) return mergeWavBuffers(buffers);
|
|
276
384
|
if (isMp3Format(format)) {
|
|
277
385
|
const parts = buffers.map(stripMp3Tags);
|
|
@@ -294,7 +402,8 @@ function mergeAudioBuffers(buffers, options) {
|
|
|
294
402
|
}
|
|
295
403
|
throw new UnsupportedMergeFormatError(format);
|
|
296
404
|
} catch (error) {
|
|
297
|
-
if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError)
|
|
405
|
+
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
406
|
+
throw error;
|
|
298
407
|
throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
|
|
299
408
|
}
|
|
300
409
|
}
|
|
@@ -372,17 +481,26 @@ async function synthesizeSsml(ssml, config) {
|
|
|
372
481
|
return {
|
|
373
482
|
originalTextRange: { ...marker.originalTextRange },
|
|
374
483
|
sourceNodePath: [...marker.sourceNodePath],
|
|
375
|
-
textRange: { ...marker.originalTextRange }
|
|
484
|
+
textRange: { ...marker.originalTextRange },
|
|
485
|
+
mappingStatus: "exact"
|
|
376
486
|
};
|
|
377
487
|
}
|
|
378
|
-
if (sourceSegments.length === 0 && !config.sourceTextRange && !config.sourceNodePath)
|
|
488
|
+
if (sourceSegments.length === 0 && !config.sourceTextRange && !config.sourceNodePath) {
|
|
489
|
+
const unmapped = { mappingStatus: "unmapped" };
|
|
490
|
+
Object.defineProperty(unmapped, "mappingStatus", { value: "unmapped", enumerable: false });
|
|
491
|
+
return unmapped;
|
|
492
|
+
}
|
|
379
493
|
const value = text ?? "";
|
|
380
494
|
let localStart = Number.isFinite(offsetHint) && (offsetHint ?? 0) >= 0 ? offsetHint : -1;
|
|
381
|
-
|
|
495
|
+
let mappingStatus = "exact";
|
|
496
|
+
if (value && localStart >= 0 && sourceText.slice(localStart, localStart + value.length) !== value) {
|
|
382
497
|
localStart = -1;
|
|
498
|
+
mappingStatus = "fallback";
|
|
499
|
+
}
|
|
383
500
|
if (localStart < 0 || localStart > sourceText.length) {
|
|
384
501
|
localStart = value ? sourceText.indexOf(value, sourceEventCursor) : sourceEventCursor;
|
|
385
502
|
if (localStart < 0) localStart = value ? sourceText.indexOf(value) : sourceEventCursor;
|
|
503
|
+
mappingStatus = "fallback";
|
|
386
504
|
}
|
|
387
505
|
localStart = Math.max(0, localStart);
|
|
388
506
|
const localEnd = Math.min(sourceText.length, localStart + value.length);
|
|
@@ -393,7 +511,8 @@ async function synthesizeSsml(ssml, config) {
|
|
|
393
511
|
return {
|
|
394
512
|
originalTextRange: { ...fallbackRange },
|
|
395
513
|
textRange: { ...fallbackRange },
|
|
396
|
-
...segment ? { sourceNodePath: [...segment.sourceNodePath] } : config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {}
|
|
514
|
+
...segment ? { sourceNodePath: [...segment.sourceNodePath] } : config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
|
|
515
|
+
mappingStatus: segment || config.sourceTextRange || config.sourceNodePath ? mappingStatus : "unmapped"
|
|
397
516
|
};
|
|
398
517
|
};
|
|
399
518
|
synthesizer.wordBoundary = (_sender, event) => {
|
|
@@ -442,20 +561,27 @@ async function synthesizeSsml(ssml, config) {
|
|
|
442
561
|
);
|
|
443
562
|
const durationMs = result.audioDuration ? ticksToMilliseconds(result.audioDuration) : eventDurationMs;
|
|
444
563
|
const requestId = result.resultId;
|
|
445
|
-
const addSourceMetadata = (event) =>
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
564
|
+
const addSourceMetadata = (event) => {
|
|
565
|
+
const mapped = {
|
|
566
|
+
...event,
|
|
567
|
+
...config.sourceTextRange && !("textRange" in event) ? { textRange: { ...config.sourceTextRange } } : {},
|
|
568
|
+
...config.sourceTextRange && !("originalTextRange" in event) ? { originalTextRange: { ...config.sourceTextRange } } : {},
|
|
569
|
+
...config.chunkIndex !== void 0 ? { chunkIndex: config.chunkIndex } : {},
|
|
570
|
+
...config.sourceNodePath ? { sourceNodePath: [...config.sourceNodePath] } : {},
|
|
571
|
+
...requestId ? { requestId } : {}
|
|
572
|
+
};
|
|
573
|
+
if (event.mappingStatus === "unmapped")
|
|
574
|
+
Object.defineProperty(mapped, "mappingStatus", { value: "unmapped", enumerable: false });
|
|
575
|
+
return mapped;
|
|
576
|
+
};
|
|
453
577
|
const sourceBoundaries = boundaries.map((boundary) => addSourceMetadata(boundary));
|
|
454
578
|
const sourceVisemes = visemes.map((viseme) => addSourceMetadata(viseme));
|
|
455
579
|
const sourceBookmarks = bookmarks.map((bookmark) => addSourceMetadata(bookmark));
|
|
456
580
|
resolve({
|
|
457
581
|
audioData: result.audioData,
|
|
458
582
|
durationMs,
|
|
583
|
+
audioSpec: inspectAudioSpecification(result.audioData, config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
|
|
584
|
+
mimeType: resolveMimeType(config.outputFormat ?? DEFAULT_OUTPUT_FORMAT),
|
|
459
585
|
...config.sourceTextRange ? { textRange: { ...config.sourceTextRange } } : {},
|
|
460
586
|
...requestId ? { requestId } : {},
|
|
461
587
|
...sourceBoundaries.length > 0 ? { boundaries: sourceBoundaries, wordBoundary: sourceBoundaries, wordBoundaries: sourceBoundaries } : {},
|
|
@@ -480,8 +606,66 @@ async function synthesizeSsml(ssml, config) {
|
|
|
480
606
|
}
|
|
481
607
|
});
|
|
482
608
|
}
|
|
609
|
+
function isRetryableSynthesisError(error) {
|
|
610
|
+
if (error instanceof SynthesisCancelledError || error instanceof SynthesisTimeoutError) return false;
|
|
611
|
+
if (error instanceof AzureTtsError && error.status !== 0)
|
|
612
|
+
return error.status === 429 || error.status >= 500 && error.status < 600;
|
|
613
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
614
|
+
if (/\b4\d{2}\b/.test(message)) return false;
|
|
615
|
+
const status = error && typeof error === "object" && "status" in error ? error.status : void 0;
|
|
616
|
+
if (typeof status === "number") return status === 429 || status >= 500 && status < 600;
|
|
617
|
+
return /network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message);
|
|
618
|
+
}
|
|
619
|
+
function retryDelay(options, retryAttempt) {
|
|
620
|
+
const base = Math.min(options.maxDelayMs, options.initialDelayMs * 2 ** Math.max(0, retryAttempt - 1));
|
|
621
|
+
return Math.floor(Math.random() * (base + 1));
|
|
622
|
+
}
|
|
623
|
+
function resolveConcurrency(value, total) {
|
|
624
|
+
if (value === void 0) return 1;
|
|
625
|
+
if (value === Infinity) return Math.max(1, total);
|
|
626
|
+
return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
|
|
627
|
+
}
|
|
628
|
+
async function waitForRetry(delayMs, signal) {
|
|
629
|
+
if (signal?.aborted) throw new SynthesisCancelledError();
|
|
630
|
+
if (delayMs <= 0) return;
|
|
631
|
+
await new Promise((resolve, reject) => {
|
|
632
|
+
let timer;
|
|
633
|
+
const abort = () => {
|
|
634
|
+
clearTimeout(timer);
|
|
635
|
+
signal?.removeEventListener("abort", abort);
|
|
636
|
+
reject(new SynthesisCancelledError());
|
|
637
|
+
};
|
|
638
|
+
timer = setTimeout(() => {
|
|
639
|
+
signal?.removeEventListener("abort", abort);
|
|
640
|
+
resolve();
|
|
641
|
+
}, delayMs);
|
|
642
|
+
if (signal) {
|
|
643
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
async function synthesizeWithRetry(ssml, config, retryOptions, onRetry) {
|
|
648
|
+
const options = retryOptions ? {
|
|
649
|
+
maxRetries: Math.max(0, Math.floor(retryOptions.maxRetries)),
|
|
650
|
+
initialDelayMs: Math.max(0, retryOptions.initialDelayMs),
|
|
651
|
+
maxDelayMs: Math.max(0, retryOptions.maxDelayMs)
|
|
652
|
+
} : void 0;
|
|
653
|
+
let attempt = 0;
|
|
654
|
+
while (true) {
|
|
655
|
+
if (config.signal?.aborted) throw new SynthesisCancelledError();
|
|
656
|
+
try {
|
|
657
|
+
return await synthesizeSsml(ssml, config);
|
|
658
|
+
} catch (error) {
|
|
659
|
+
if (!options || attempt >= options.maxRetries || !isRetryableSynthesisError(error)) throw error;
|
|
660
|
+
attempt += 1;
|
|
661
|
+
const delayMs = retryDelay(options, attempt);
|
|
662
|
+
onRetry(attempt, delayMs);
|
|
663
|
+
await waitForRetry(delayMs, config.signal);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
483
667
|
async function synthesizeSsmlChunks(chunks, config) {
|
|
484
|
-
const results =
|
|
668
|
+
const results = new Array(chunks.length);
|
|
485
669
|
const totalChunks = chunks.length;
|
|
486
670
|
const report = (event) => config.onProgress?.(event);
|
|
487
671
|
for (const [index, chunk] of chunks.entries()) {
|
|
@@ -496,57 +680,85 @@ async function synthesizeSsmlChunks(chunks, config) {
|
|
|
496
680
|
durationMs: 0
|
|
497
681
|
});
|
|
498
682
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
durationMs: 0
|
|
509
|
-
});
|
|
510
|
-
const startedAt = Date.now();
|
|
511
|
-
try {
|
|
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) {
|
|
683
|
+
let completed = 0;
|
|
684
|
+
let nextIndex = 0;
|
|
685
|
+
const concurrency = resolveConcurrency(config.concurrency, chunks.length);
|
|
686
|
+
const worker = async () => {
|
|
687
|
+
while (true) {
|
|
688
|
+
const index = nextIndex++;
|
|
689
|
+
if (index >= chunks.length) return;
|
|
690
|
+
const chunk = chunks[index];
|
|
691
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
532
692
|
report({
|
|
533
|
-
currentChunk:
|
|
693
|
+
currentChunk: completed,
|
|
534
694
|
totalChunks,
|
|
535
|
-
percent: totalChunks === 0 ? 100 : Math.round(
|
|
695
|
+
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
536
696
|
chunkIndex: index,
|
|
537
697
|
originalTextRange: input.originalTextRange,
|
|
538
|
-
status: "
|
|
539
|
-
durationMs:
|
|
540
|
-
error
|
|
698
|
+
status: "synthesizing",
|
|
699
|
+
durationMs: 0
|
|
541
700
|
});
|
|
542
|
-
|
|
701
|
+
const startedAt = Date.now();
|
|
702
|
+
try {
|
|
703
|
+
const result = await synthesizeWithRetry(
|
|
704
|
+
input.ssml,
|
|
705
|
+
{
|
|
706
|
+
...config,
|
|
707
|
+
...input.originalTextRange ? { sourceTextRange: input.originalTextRange } : {},
|
|
708
|
+
...input.sourceNodePath ?? config.sourceNodePath ? { sourceNodePath: [...input.sourceNodePath ?? config.sourceNodePath ?? []] } : {},
|
|
709
|
+
...input.sourceTextSegments ? { sourceTextSegments: input.sourceTextSegments } : {},
|
|
710
|
+
...input.sourceMarkers ? { sourceMarkers: input.sourceMarkers } : {},
|
|
711
|
+
chunkIndex: index,
|
|
712
|
+
onProgress: void 0
|
|
713
|
+
},
|
|
714
|
+
config.retryOptions,
|
|
715
|
+
(retryAttempt, nextRetryDelayMs) => report({
|
|
716
|
+
currentChunk: completed,
|
|
717
|
+
totalChunks,
|
|
718
|
+
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
719
|
+
chunkIndex: index,
|
|
720
|
+
originalTextRange: input.originalTextRange,
|
|
721
|
+
status: "synthesizing",
|
|
722
|
+
durationMs: Date.now() - startedAt,
|
|
723
|
+
retryAttempt,
|
|
724
|
+
nextRetryDelayMs,
|
|
725
|
+
isRetrying: true
|
|
726
|
+
})
|
|
727
|
+
);
|
|
728
|
+
results[index] = result;
|
|
729
|
+
completed += 1;
|
|
730
|
+
report({
|
|
731
|
+
currentChunk: completed,
|
|
732
|
+
totalChunks,
|
|
733
|
+
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
734
|
+
chunkIndex: index,
|
|
735
|
+
originalTextRange: input.originalTextRange,
|
|
736
|
+
status: "success",
|
|
737
|
+
durationMs: Date.now() - startedAt
|
|
738
|
+
});
|
|
739
|
+
} catch (error) {
|
|
740
|
+
report({
|
|
741
|
+
currentChunk: completed,
|
|
742
|
+
totalChunks,
|
|
743
|
+
percent: totalChunks === 0 ? 100 : Math.round(completed / totalChunks * 100),
|
|
744
|
+
chunkIndex: index,
|
|
745
|
+
originalTextRange: input.originalTextRange,
|
|
746
|
+
status: "failed",
|
|
747
|
+
durationMs: Date.now() - startedAt,
|
|
748
|
+
error
|
|
749
|
+
});
|
|
750
|
+
throw error;
|
|
751
|
+
}
|
|
543
752
|
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
|
|
753
|
+
};
|
|
754
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
755
|
+
const orderedResults = results.filter((result) => result !== void 0);
|
|
756
|
+
return mergeSynthesisResults(orderedResults, {
|
|
757
|
+
format: config.outputFormat ?? DEFAULT_OUTPUT_FORMAT,
|
|
758
|
+
signal: config.signal
|
|
547
759
|
});
|
|
548
760
|
}
|
|
549
|
-
function createMergedResult(results, audioData, format) {
|
|
761
|
+
function createMergedResult(results, audioData, format, audioSpec, outputMimeType) {
|
|
550
762
|
const boundaries = [];
|
|
551
763
|
const visemes = [];
|
|
552
764
|
const bookmarks = [];
|
|
@@ -565,7 +777,8 @@ function createMergedResult(results, audioData, format) {
|
|
|
565
777
|
...boundary.sourceNodePath ? { sourceNodePath: [...boundary.sourceNodePath] } : {},
|
|
566
778
|
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
567
779
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
568
|
-
...requestId ? { requestId } : {}
|
|
780
|
+
...requestId ? { requestId } : {},
|
|
781
|
+
mappingStatus: boundary.mappingStatus ?? "unmapped"
|
|
569
782
|
});
|
|
570
783
|
}
|
|
571
784
|
for (const viseme of result.visemes ?? []) {
|
|
@@ -580,7 +793,8 @@ function createMergedResult(results, audioData, format) {
|
|
|
580
793
|
...viseme.sourceNodePath ? { sourceNodePath: [...viseme.sourceNodePath] } : {},
|
|
581
794
|
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
582
795
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
583
|
-
...requestId ? { requestId } : {}
|
|
796
|
+
...requestId ? { requestId } : {},
|
|
797
|
+
mappingStatus: viseme.mappingStatus ?? "unmapped"
|
|
584
798
|
});
|
|
585
799
|
}
|
|
586
800
|
for (const bookmark of result.bookmarks ?? []) {
|
|
@@ -595,7 +809,8 @@ function createMergedResult(results, audioData, format) {
|
|
|
595
809
|
...bookmark.sourceNodePath ? { sourceNodePath: [...bookmark.sourceNodePath] } : {},
|
|
596
810
|
...originalTextRange ? { originalTextRange: { ...originalTextRange } } : {},
|
|
597
811
|
...textRange ? { textRange: { ...textRange } } : {},
|
|
598
|
-
...requestId ? { requestId } : {}
|
|
812
|
+
...requestId ? { requestId } : {},
|
|
813
|
+
mappingStatus: bookmark.mappingStatus ?? "unmapped"
|
|
599
814
|
});
|
|
600
815
|
}
|
|
601
816
|
durationOffset += Math.max(0, result.durationMs);
|
|
@@ -604,6 +819,8 @@ function createMergedResult(results, audioData, format) {
|
|
|
604
819
|
audioData,
|
|
605
820
|
durationMs: durationOffset,
|
|
606
821
|
mimeType: resolveMimeType(format),
|
|
822
|
+
audioSpec: audioSpec ?? formatAudioSpecification(format),
|
|
823
|
+
...outputMimeType ? { mimeType: outputMimeType } : {},
|
|
607
824
|
...boundaries.length > 0 ? { boundaries, wordBoundary: boundaries, wordBoundaries: boundaries } : {},
|
|
608
825
|
...visemes.length > 0 ? { visemes } : {},
|
|
609
826
|
...bookmarks.length > 0 ? { bookmarks } : {},
|
|
@@ -616,19 +833,47 @@ function mergeSynthesisResults(results, options) {
|
|
|
616
833
|
const format = resolvedOptions?.format;
|
|
617
834
|
if (!format) throw new UnsupportedMergeFormatError("");
|
|
618
835
|
const buffers = results.map((result) => result.audioData);
|
|
836
|
+
const inputSpecs = results.map((result) => result.audioSpec ?? inspectAudioSpecification(result.audioData, format));
|
|
837
|
+
validateAudioSpecifications(inputSpecs);
|
|
838
|
+
const signal = resolvedOptions.signal ?? new AbortController().signal;
|
|
839
|
+
if (signal.aborted) throw new SynthesisCancelledError();
|
|
619
840
|
if (resolvedOptions.customMerger) {
|
|
620
|
-
return Promise.resolve().then(
|
|
841
|
+
return Promise.resolve().then(
|
|
842
|
+
() => resolvedOptions.customMerger?.(buffers, {
|
|
843
|
+
format,
|
|
844
|
+
outputMimeType: resolvedOptions.outputMimeType ?? resolveMimeType(format),
|
|
845
|
+
inputSpecs,
|
|
846
|
+
signal
|
|
847
|
+
})
|
|
848
|
+
).then((merged) => {
|
|
621
849
|
if (!merged) throw new MergeError("The custom audio merger returned no audio buffer.");
|
|
622
|
-
|
|
850
|
+
if (!(merged instanceof ArrayBuffer) || buffers.some((buffer) => buffer.byteLength > 0) && merged.byteLength === 0)
|
|
851
|
+
throw new MergeError("The custom audio merger returned an invalid audio buffer.");
|
|
852
|
+
if (signal.aborted) throw new SynthesisCancelledError();
|
|
853
|
+
return createMergedResult(
|
|
854
|
+
results,
|
|
855
|
+
merged,
|
|
856
|
+
format,
|
|
857
|
+
inspectAudioSpecification(merged, format),
|
|
858
|
+
resolvedOptions.outputMimeType
|
|
859
|
+
);
|
|
623
860
|
}).catch((error) => {
|
|
624
|
-
if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError)
|
|
861
|
+
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
862
|
+
throw error;
|
|
625
863
|
throw new MergeError(`Custom audio merger failed for format "${format}".`, error);
|
|
626
864
|
});
|
|
627
865
|
}
|
|
628
866
|
try {
|
|
629
|
-
return createMergedResult(
|
|
867
|
+
return createMergedResult(
|
|
868
|
+
results,
|
|
869
|
+
mergeAudioBuffers(buffers, { format }),
|
|
870
|
+
format,
|
|
871
|
+
inputSpecs[0],
|
|
872
|
+
resolvedOptions.outputMimeType
|
|
873
|
+
);
|
|
630
874
|
} catch (error) {
|
|
631
|
-
if (error instanceof UnsupportedMergeFormatError || error instanceof MergeError)
|
|
875
|
+
if (error instanceof UnsupportedMergeFormatError || isAudioFormatMismatch(error) || error instanceof MergeError)
|
|
876
|
+
throw error;
|
|
632
877
|
throw new MergeError(`Audio buffers could not be merged for format "${format}".`, error);
|
|
633
878
|
}
|
|
634
879
|
}
|
|
@@ -637,7 +882,10 @@ async function synthesizeSpeech(ssml, config) {
|
|
|
637
882
|
}
|
|
638
883
|
|
|
639
884
|
// src/safe.ts
|
|
640
|
-
import {
|
|
885
|
+
import {
|
|
886
|
+
createAzureUrlValidatorRunner,
|
|
887
|
+
validateAzureSsml
|
|
888
|
+
} from "@ssml-builder-js/ssml-core";
|
|
641
889
|
var ChunkValidationError = class extends Error {
|
|
642
890
|
constructor(chunkIndex, diagnostics) {
|
|
643
891
|
super(`SSML validation failed for chunk ${chunkIndex}; the Azure Speech API was not called.`);
|
|
@@ -650,8 +898,73 @@ var ChunkValidationError = class extends Error {
|
|
|
650
898
|
function failure(error) {
|
|
651
899
|
return { ok: false, success: false, status: error.kind, error };
|
|
652
900
|
}
|
|
901
|
+
function isRetryable(error) {
|
|
902
|
+
if (error instanceof Error && /cancel|abort|tim(?:e|ed) ?out/i.test(error.message)) return false;
|
|
903
|
+
const status = error && typeof error === "object" && "status" in error ? error.status : void 0;
|
|
904
|
+
if (typeof status === "number" && status !== 0) return status === 429 || status >= 500 && status < 600;
|
|
905
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
906
|
+
if (/\b4\d{2}\b/.test(message)) return false;
|
|
907
|
+
return /network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message);
|
|
908
|
+
}
|
|
909
|
+
function delayForRetry(options, attempt) {
|
|
910
|
+
const maxDelay = Math.max(0, options.maxDelayMs);
|
|
911
|
+
const base = Math.min(maxDelay, Math.max(0, options.initialDelayMs) * 2 ** Math.max(0, attempt - 1));
|
|
912
|
+
return Math.floor(Math.random() * (base + 1));
|
|
913
|
+
}
|
|
914
|
+
function resolveConcurrency2(value, total) {
|
|
915
|
+
if (value === void 0) return 1;
|
|
916
|
+
if (value === Infinity) return Math.max(1, total);
|
|
917
|
+
return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
|
|
918
|
+
}
|
|
919
|
+
async function retryableSynthesis(synthesize, options, signal, onRetry) {
|
|
920
|
+
const retry = options ? {
|
|
921
|
+
maxRetries: Math.max(0, Math.floor(options.maxRetries)),
|
|
922
|
+
initialDelayMs: options.initialDelayMs,
|
|
923
|
+
maxDelayMs: options.maxDelayMs
|
|
924
|
+
} : void 0;
|
|
925
|
+
let attempt = 0;
|
|
926
|
+
while (true) {
|
|
927
|
+
if (signal?.aborted) throw new Error("Speech synthesis was cancelled.");
|
|
928
|
+
try {
|
|
929
|
+
return await synthesize();
|
|
930
|
+
} catch (error) {
|
|
931
|
+
if (!retry || attempt >= retry.maxRetries || !isRetryable(error)) throw error;
|
|
932
|
+
attempt += 1;
|
|
933
|
+
const delayMs = delayForRetry(retry, attempt);
|
|
934
|
+
onRetry(attempt, delayMs);
|
|
935
|
+
if (delayMs > 0)
|
|
936
|
+
await new Promise((resolve, reject) => {
|
|
937
|
+
const timer = setTimeout(() => {
|
|
938
|
+
signal?.removeEventListener("abort", abort);
|
|
939
|
+
resolve();
|
|
940
|
+
}, delayMs);
|
|
941
|
+
const abort = () => {
|
|
942
|
+
clearTimeout(timer);
|
|
943
|
+
signal?.removeEventListener("abort", abort);
|
|
944
|
+
reject(new Error("Speech synthesis was cancelled."));
|
|
945
|
+
};
|
|
946
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
function sharedValidationOptions(options, signal) {
|
|
952
|
+
const validator = options.urlValidator ?? options.customUrlValidator;
|
|
953
|
+
if (!validator) return signal ? withValidationSignal(options, signal) : options;
|
|
954
|
+
const runner = createAzureUrlValidatorRunner(validator, {
|
|
955
|
+
...options.urlValidation ?? {},
|
|
956
|
+
...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
|
|
957
|
+
...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
|
|
958
|
+
...signal ? { signal } : {},
|
|
959
|
+
...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
|
|
960
|
+
});
|
|
961
|
+
return {
|
|
962
|
+
...withValidationSignal(options, signal),
|
|
963
|
+
urlValidatorRunner: runner
|
|
964
|
+
};
|
|
965
|
+
}
|
|
653
966
|
async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
654
|
-
const validationOptions =
|
|
967
|
+
const validationOptions = sharedValidationOptions(options.validation ?? options, options.signal);
|
|
655
968
|
const diagnostics = await Promise.resolve(validateAzureSsml(ssml, validationOptions));
|
|
656
969
|
if (options.signal?.aborted) {
|
|
657
970
|
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
@@ -678,7 +991,7 @@ async function synthesizeSsmlSafe(client, ssml, options = {}) {
|
|
|
678
991
|
}
|
|
679
992
|
}
|
|
680
993
|
async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
681
|
-
const validationOptions =
|
|
994
|
+
const validationOptions = sharedValidationOptions(options.validation ?? options, options.signal);
|
|
682
995
|
if (options.signal?.aborted) {
|
|
683
996
|
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
684
997
|
return failure(error);
|
|
@@ -699,16 +1012,24 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
699
1012
|
pending(index, "pending");
|
|
700
1013
|
});
|
|
701
1014
|
const validations = await Promise.all(
|
|
702
|
-
chunks.map(async (chunk) => {
|
|
1015
|
+
chunks.map(async (chunk, index) => {
|
|
703
1016
|
const ssml = typeof chunk === "string" ? chunk : chunk.ssml;
|
|
704
1017
|
const sourceNodePath = typeof chunk === "string" ? options.sourceNodePath : chunk.sourceNodePath ?? options.sourceNodePath;
|
|
705
1018
|
const diagnostics = await Promise.resolve(
|
|
706
|
-
validateAzureSsml(ssml, {
|
|
1019
|
+
validateAzureSsml(ssml, {
|
|
1020
|
+
...validationOptions,
|
|
1021
|
+
...sourceNodePath ? { sourceNodePath } : {},
|
|
1022
|
+
chunkIndex: index
|
|
1023
|
+
})
|
|
707
1024
|
);
|
|
708
1025
|
return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
709
1026
|
})
|
|
710
1027
|
);
|
|
711
1028
|
const firstInvalidIndex = validations.findIndex((diagnostics) => diagnostics.length > 0);
|
|
1029
|
+
if (options.signal?.aborted) {
|
|
1030
|
+
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
1031
|
+
return failure(error);
|
|
1032
|
+
}
|
|
712
1033
|
if (firstInvalidIndex >= 0) {
|
|
713
1034
|
const error = new ChunkValidationError(firstInvalidIndex, validations[firstInvalidIndex] ?? []);
|
|
714
1035
|
pending(firstInvalidIndex, "failed", error);
|
|
@@ -725,96 +1046,126 @@ async function synthesizeSsmlChunksSafe(client, chunks, options = {}) {
|
|
|
725
1046
|
outputFormat: options.outputFormat,
|
|
726
1047
|
signal: options.signal,
|
|
727
1048
|
timeoutMs: options.timeoutMs,
|
|
728
|
-
sourceNodePath: options.sourceNodePath
|
|
1049
|
+
sourceNodePath: options.sourceNodePath,
|
|
1050
|
+
concurrency: options.concurrency,
|
|
1051
|
+
retryOptions: options.retryOptions
|
|
729
1052
|
});
|
|
730
1053
|
return { ok: true, success: true, status: "success", value };
|
|
731
1054
|
}
|
|
732
|
-
const results =
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
sourceNodePath:
|
|
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
|
-
|
|
1055
|
+
const results = new Array(chunks.length);
|
|
1056
|
+
let completed = 0;
|
|
1057
|
+
let nextIndex = 0;
|
|
1058
|
+
const concurrency = resolveConcurrency2(options.concurrency, chunks.length);
|
|
1059
|
+
const worker = async () => {
|
|
1060
|
+
while (true) {
|
|
1061
|
+
const index = nextIndex++;
|
|
1062
|
+
if (index >= chunks.length) return;
|
|
1063
|
+
const chunk = chunks[index];
|
|
1064
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
1065
|
+
const sourceNodePath = input.sourceNodePath;
|
|
1066
|
+
const originalTextRange = input.originalTextRange;
|
|
1067
|
+
pending(index, "synthesizing");
|
|
1068
|
+
const startedAt = Date.now();
|
|
1069
|
+
try {
|
|
1070
|
+
const result = await retryableSynthesis(
|
|
1071
|
+
() => client.synthesizeSsml(input.ssml, {
|
|
1072
|
+
outputFormat: options.outputFormat,
|
|
1073
|
+
signal: options.signal,
|
|
1074
|
+
timeoutMs: options.timeoutMs,
|
|
1075
|
+
sourceNodePath: input.sourceNodePath ?? options.sourceNodePath
|
|
1076
|
+
}),
|
|
1077
|
+
options.retryOptions,
|
|
1078
|
+
options.signal,
|
|
1079
|
+
(retryAttempt, nextRetryDelayMs) => options.onProgress?.({
|
|
1080
|
+
currentChunk: completed,
|
|
1081
|
+
totalChunks: chunks.length,
|
|
1082
|
+
percent: chunks.length === 0 ? 100 : Math.round(completed / chunks.length * 100),
|
|
1083
|
+
chunkIndex: index,
|
|
1084
|
+
originalTextRange: input.originalTextRange,
|
|
1085
|
+
status: "synthesizing",
|
|
1086
|
+
durationMs: Date.now() - startedAt,
|
|
1087
|
+
retryAttempt,
|
|
1088
|
+
nextRetryDelayMs,
|
|
1089
|
+
isRetrying: true
|
|
1090
|
+
})
|
|
1091
|
+
);
|
|
1092
|
+
results[index] = {
|
|
1093
|
+
...result,
|
|
1094
|
+
...input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {},
|
|
1095
|
+
...sourceNodePath ? {
|
|
1096
|
+
boundaries: result.boundaries?.map((event) => ({
|
|
1097
|
+
...event,
|
|
1098
|
+
sourceNodePath: [...sourceNodePath],
|
|
1099
|
+
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
1100
|
+
})),
|
|
1101
|
+
visemes: result.visemes?.map((event) => ({
|
|
1102
|
+
...event,
|
|
1103
|
+
sourceNodePath: [...sourceNodePath],
|
|
1104
|
+
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
1105
|
+
})),
|
|
1106
|
+
bookmarks: result.bookmarks?.map((event) => ({
|
|
1107
|
+
...event,
|
|
1108
|
+
sourceNodePath: [...sourceNodePath],
|
|
1109
|
+
...event.originalTextRange ? { originalTextRange: { ...event.originalTextRange } } : input.originalTextRange ? { originalTextRange: { ...input.originalTextRange } } : {}
|
|
1110
|
+
}))
|
|
1111
|
+
} : {},
|
|
1112
|
+
...originalTextRange ? {
|
|
1113
|
+
boundaries: result.boundaries?.map((event) => ({
|
|
1114
|
+
...event,
|
|
1115
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1116
|
+
})),
|
|
1117
|
+
wordBoundary: result.wordBoundary?.map((event) => ({
|
|
1118
|
+
...event,
|
|
1119
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1120
|
+
})),
|
|
1121
|
+
wordBoundaries: result.wordBoundaries?.map((event) => ({
|
|
1122
|
+
...event,
|
|
1123
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1124
|
+
})),
|
|
1125
|
+
visemes: result.visemes?.map((event) => ({
|
|
1126
|
+
...event,
|
|
1127
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1128
|
+
})),
|
|
1129
|
+
bookmarks: result.bookmarks?.map((event) => ({
|
|
1130
|
+
...event,
|
|
1131
|
+
originalTextRange: event.originalTextRange ? { ...event.originalTextRange } : { ...originalTextRange }
|
|
1132
|
+
}))
|
|
1133
|
+
} : {}
|
|
1134
|
+
};
|
|
1135
|
+
completed += 1;
|
|
1136
|
+
options.onProgress?.({
|
|
1137
|
+
currentChunk: completed,
|
|
1138
|
+
totalChunks: chunks.length,
|
|
1139
|
+
percent: chunks.length === 0 ? 100 : Math.round(completed / chunks.length * 100),
|
|
1140
|
+
chunkIndex: index,
|
|
1141
|
+
originalTextRange: input.originalTextRange,
|
|
1142
|
+
status: "success",
|
|
1143
|
+
durationMs: Date.now() - startedAt
|
|
1144
|
+
});
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
options.onProgress?.({
|
|
1147
|
+
currentChunk: completed,
|
|
1148
|
+
totalChunks: chunks.length,
|
|
1149
|
+
percent: chunks.length === 0 ? 100 : Math.round(index / chunks.length * 100),
|
|
1150
|
+
chunkIndex: index,
|
|
1151
|
+
originalTextRange: input.originalTextRange,
|
|
1152
|
+
status: "failed",
|
|
1153
|
+
durationMs: Date.now() - startedAt,
|
|
1154
|
+
error
|
|
1155
|
+
});
|
|
1156
|
+
throw error;
|
|
1157
|
+
}
|
|
810
1158
|
}
|
|
811
|
-
}
|
|
1159
|
+
};
|
|
1160
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
1161
|
+
const orderedResults = results.filter((result) => result !== void 0);
|
|
812
1162
|
return {
|
|
813
1163
|
ok: true,
|
|
814
1164
|
success: true,
|
|
815
1165
|
status: "success",
|
|
816
|
-
value: mergeSynthesisResults(
|
|
817
|
-
format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3"
|
|
1166
|
+
value: mergeSynthesisResults(orderedResults, {
|
|
1167
|
+
format: options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3",
|
|
1168
|
+
signal: options.signal
|
|
818
1169
|
})
|
|
819
1170
|
};
|
|
820
1171
|
} catch (error) {
|
|
@@ -873,7 +1224,9 @@ var AzureTtsClient = class {
|
|
|
873
1224
|
signal: options.signal ?? signal,
|
|
874
1225
|
timeoutMs: options.timeoutMs ?? timeoutMs,
|
|
875
1226
|
sourceNodePath: options.sourceNodePath,
|
|
876
|
-
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
|
|
1227
|
+
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
1228
|
+
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
1229
|
+
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions
|
|
877
1230
|
});
|
|
878
1231
|
}
|
|
879
1232
|
async synthesizeSsmlSafe(ssml, options = {}) {
|
|
@@ -885,7 +1238,9 @@ var AzureTtsClient = class {
|
|
|
885
1238
|
outputFormat: options.outputFormat ?? __privateGet(this, _options).outputFormat,
|
|
886
1239
|
signal: options.signal ?? __privateGet(this, _options).signal,
|
|
887
1240
|
timeoutMs: options.timeoutMs ?? __privateGet(this, _options).timeoutMs,
|
|
888
|
-
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress
|
|
1241
|
+
onProgress: options.onProgress ?? __privateGet(this, _options).onProgress,
|
|
1242
|
+
concurrency: options.concurrency ?? __privateGet(this, _options).concurrency,
|
|
1243
|
+
retryOptions: options.retryOptions ?? __privateGet(this, _options).retryOptions
|
|
889
1244
|
});
|
|
890
1245
|
}
|
|
891
1246
|
async synthesizeSsmlChunksSafe(chunks, options = {}) {
|
|
@@ -981,6 +1336,7 @@ async function fetchAzureVoiceCatalog(options) {
|
|
|
981
1336
|
};
|
|
982
1337
|
}
|
|
983
1338
|
export {
|
|
1339
|
+
AudioFormatMismatchError,
|
|
984
1340
|
AzureTtsClient,
|
|
985
1341
|
AzureTtsError,
|
|
986
1342
|
AzureTtsSdkError,
|
|
@@ -992,6 +1348,7 @@ export {
|
|
|
992
1348
|
UnsupportedMergeFormatError,
|
|
993
1349
|
canMergeAudioFormat,
|
|
994
1350
|
fetchAzureVoiceCatalog,
|
|
1351
|
+
inspectAudioSpecification,
|
|
995
1352
|
mergeAudioBuffers,
|
|
996
1353
|
mergeSynthesisResults,
|
|
997
1354
|
resolveMergeAudioFormat,
|